diff --git a/packages/http/lib/http.dart b/packages/http/lib/http.dart index 397ba63..23f7d0d 100644 --- a/packages/http/lib/http.dart +++ b/packages/http/lib/http.dart @@ -1,3 +1,26 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// This library exports various components and utilities for handling HTTP requests and responses, +/// including controllers, request/response processing, routing, and serialization. +/// +/// It provides a comprehensive set of tools for building web applications and APIs, including: +/// - Request and response handling +/// - Body decoding and encoding +/// - Caching and CORS policies +/// - File handling +/// - Database object controllers +/// - Resource controllers with bindings and scopes +/// - Routing and request path processing +/// - Serialization utilities +library; + export 'src/body_decoder.dart'; export 'src/cache_policy.dart'; export 'src/controller.dart'; diff --git a/packages/http/lib/src/body_decoder.dart b/packages/http/lib/src/body_decoder.dart index ff715e5..6595b77 100644 --- a/packages/http/lib/src/body_decoder.dart +++ b/packages/http/lib/src/body_decoder.dart @@ -1,14 +1,26 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:convert'; import 'dart:io'; - import 'package:protevus_http/http.dart'; import 'package:protevus_runtime/runtime.dart'; -/// Decodes [bytes] according to [contentType]. +/// A class that decodes bytes according to a specific content type. /// -/// See [RequestBody] for a concrete implementation. +/// This abstract class provides the base functionality for decoding byte streams +/// based on their content type. abstract class BodyDecoder { + /// Creates a new [BodyDecoder] instance. + /// + /// [bodyByteStream] is the stream of bytes to be decoded. BodyDecoder(Stream> bodyByteStream) : _originalByteStream = bodyByteStream; @@ -67,8 +79,13 @@ abstract class BodyDecoder { return _bytes; } + /// The original byte stream to be decoded. final Stream> _originalByteStream; + + /// The decoded data after processing. dynamic _decodedData; + + /// The original bytes, if retained. List? _bytes; /// Decodes this object's bytes as [T]. @@ -127,6 +144,9 @@ abstract class BodyDecoder { return _cast(_decodedData); } + /// Casts the decoded body to the specified type [T]. + /// + /// Throws a [Response.badRequest] if the casting fails. T _cast(dynamic body) { try { return RuntimeContext.current.coerce(body); @@ -137,6 +157,7 @@ abstract class BodyDecoder { } } + /// Reads all bytes from the given [stream] and returns them as a [List]. Future> _readBytes(Stream> stream) async { return (await stream.toList()).expand((e) => e).toList(); } diff --git a/packages/http/lib/src/cache_policy.dart b/packages/http/lib/src/cache_policy.dart index ff883ef..fa37ed0 100644 --- a/packages/http/lib/src/cache_policy.dart +++ b/packages/http/lib/src/cache_policy.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_http/http.dart'; /// Instances of this type provide configuration for the 'Cache-Control' header. @@ -9,6 +18,11 @@ class CachePolicy { /// Policies applied to [Response.cachePolicy] will add the appropriate /// headers to that response. See properties for definitions of arguments /// to this constructor. + /// + /// [preventIntermediateProxyCaching] - If true, prevents caching by intermediate proxies. + /// [preventCaching] - If true, prevents any caching of the response. + /// [requireConditionalRequest] - If true, requires a conditional GET for cached responses. + /// [expirationFromNow] - Sets the duration for which the resource is valid. const CachePolicy({ this.preventIntermediateProxyCaching = false, this.preventCaching = false, @@ -40,6 +54,9 @@ class CachePolicy { /// Constructs a header value configured from this instance. /// /// This value is used for the 'Cache-Control' header. + /// + /// Returns a string representation of the cache control header based on the + /// configuration of this CachePolicy instance. String get headerValue { if (preventCaching) { return "no-cache, no-store"; diff --git a/packages/http/lib/src/controller.dart b/packages/http/lib/src/controller.dart index a15a654..0bdbcc7 100644 --- a/packages/http/lib/src/controller.dart +++ b/packages/http/lib/src/controller.dart @@ -1,6 +1,14 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:io'; - import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_http/http.dart'; import 'package:protevus_openapi/v3.dart'; @@ -263,6 +271,11 @@ abstract class Controller } } + /// Applies CORS headers to the response if necessary. + /// + /// This method checks if the request is a CORS request and not a preflight request. + /// If so, it applies the appropriate CORS headers to the response based on the policy + /// of the last controller in the chain. void applyCORSHeadersIfNecessary(Request req, Response resp) { if (req.isCORSRequest && !req.isPreflightRequest) { final lastPolicyController = _lastController; @@ -275,10 +288,35 @@ abstract class Controller } } + /// Documents the API paths for this controller. + /// + /// This method delegates the documentation of API paths to the next controller + /// in the chain, if one exists. If there is no next controller, it returns an + /// empty map. + /// + /// [context] is the API documentation context. + /// + /// Returns a map where the keys are path strings and the values are [APIPath] + /// objects describing the paths. @override Map documentPaths(APIDocumentContext context) => nextController?.documentPaths(context) ?? {}; + /// Documents the API operations for this controller. + /// + /// This method is responsible for generating documentation for the API operations + /// associated with this controller. It delegates the documentation process to + /// the next controller in the chain, if one exists. + /// + /// Parameters: + /// - [context]: The API documentation context. + /// - [route]: The route string for the current path. + /// - [path]: The APIPath object representing the current path. + /// + /// Returns: + /// A map where the keys are operation identifiers (typically HTTP methods) + /// and the values are [APIOperation] objects describing the operations. + /// If there is no next controller, it returns an empty map. @override Map documentOperations( APIDocumentContext context, @@ -292,10 +330,21 @@ abstract class Controller return nextController!.documentOperations(context, route, path); } + /// Documents the API components for this controller. + /// + /// This method delegates the documentation of API components to the next controller + /// in the chain, if one exists. If there is no next controller, this method does nothing. + /// + /// [context] is the API documentation context. @override void documentComponents(APIDocumentContext context) => nextController?.documentComponents(context); + /// Handles preflight requests for CORS. + /// + /// This method is called when a preflight request is received. It determines + /// which controller should handle the preflight request and delegates the + /// handling to that controller. Future? _handlePreflightRequest(Request req) async { Controller controllerToDictatePolicy; try { @@ -327,6 +376,10 @@ abstract class Controller return controllerToDictatePolicy.receive(req); } + /// Sends the response for a request. + /// + /// This method applies CORS headers if necessary, calls [willSendResponse], + /// and then sends the response. Future _sendResponse( Request request, Response response, { @@ -340,6 +393,9 @@ abstract class Controller return request.respond(response); } + /// Returns the last controller in the chain. + /// + /// This method traverses the linked controllers to find the last one in the chain. Controller get _lastController { Controller controller = this; while (controller.nextController != null) { @@ -349,6 +405,9 @@ abstract class Controller } } +/// A controller that recycles instances of another controller. +/// +/// This controller is used internally to handle controllers that implement [Recyclable]. @PreventCompilation() class _ControllerRecycler extends Controller { _ControllerRecycler(this.generator, Recyclable instance) { @@ -356,14 +415,21 @@ class _ControllerRecycler extends Controller { nextInstanceToReceive = instance; } + /// Function to generate new instances of the recyclable controller. Controller Function() generator; + + /// Override for the CORS policy. CORSPolicy? policyOverride; + + /// State to be recycled between instances. T? recycleState; Recyclable? _nextInstanceToReceive; + /// The next instance to receive requests. Recyclable? get nextInstanceToReceive => _nextInstanceToReceive; + /// Sets the next instance to receive requests and initializes it. set nextInstanceToReceive(Recyclable? instance) { _nextInstanceToReceive = instance; instance?.restore(recycleState); @@ -373,16 +439,41 @@ class _ControllerRecycler extends Controller { } } + /// Returns the CORS policy of the next instance to receive requests. + /// + /// This getter delegates to the [policy] of the [nextInstanceToReceive]. + /// If [nextInstanceToReceive] is null, this will return null. + /// + /// Returns: + /// The [CORSPolicy] of the next instance, or null if there is no next instance. @override CORSPolicy? get policy { return nextInstanceToReceive?.policy; } + /// Sets the CORS policy for this controller recycler. + /// + /// This setter overrides the CORS policy for the recycled controllers. + /// When set, it updates the [policyOverride] property, which is used + /// to apply the policy to newly generated controller instances. + /// + /// Parameters: + /// p: The [CORSPolicy] to be set. Can be null to remove the override. @override set policy(CORSPolicy? p) { policyOverride = p; } + /// Links a controller to this recycler and updates the next instance's next controller. + /// + /// This method extends the base [link] functionality by also setting the + /// [_nextController] of the [nextInstanceToReceive] to the newly linked controller. + /// + /// Parameters: + /// instantiator: A function that returns a new [Controller] instance. + /// + /// Returns: + /// The newly linked [Linkable] controller. @override Linkable link(Controller Function() instantiator) { final c = super.link(instantiator); @@ -390,6 +481,16 @@ class _ControllerRecycler extends Controller { return c; } + /// Links a function controller to this recycler and updates the next instance's next controller. + /// + /// This method extends the base [linkFunction] functionality by also setting the + /// [_nextController] of the [nextInstanceToReceive] to the newly linked function controller. + /// + /// Parameters: + /// handle: A function that takes a [Request] and returns a [FutureOr]. + /// + /// Returns: + /// The newly linked [Linkable] controller, or null if the linking failed. @override Linkable? linkFunction( FutureOr Function(Request request) handle, @@ -399,6 +500,22 @@ class _ControllerRecycler extends Controller { return c; } + /// Receives and processes an incoming request. + /// + /// This method is responsible for handling the request by delegating it to the next + /// instance in the recycling chain. It performs the following steps: + /// 1. Retrieves the current next instance to receive the request. + /// 2. Generates a new instance to be the next receiver. + /// 3. Delegates the request handling to the current next instance. + /// + /// This approach ensures that each request is handled by a fresh instance, + /// while maintaining the recycling pattern for efficient resource usage. + /// + /// Parameters: + /// req: The incoming [Request] to be processed. + /// + /// Returns: + /// A [Future] that completes when the request has been handled. @override Future? receive(Request req) { final next = nextInstanceToReceive; @@ -406,11 +523,28 @@ class _ControllerRecycler extends Controller { return next!.receive(req); } + /// This method should never be called directly on a _ControllerRecycler. + /// + /// The _ControllerRecycler is designed to delegate request handling to its + /// recycled instances. If this method is invoked, it indicates a bug in the + /// controller recycling mechanism. + /// + /// @param request The incoming request (unused in this implementation). + /// @throws StateError Always throws an error to indicate improper usage. @override FutureOr handle(Request request) { throw StateError("_ControllerRecycler invoked handle. This is a bug."); } + /// Prepares the controller for handling requests after being added to the channel. + /// + /// This method is called after the controller is added to the request handling channel, + /// but before any requests are processed. It initializes the next instance to receive + /// requests by calling its [didAddToChannel] method. + /// + /// Note: This implementation does not call the superclass method because the + /// [nextInstanceToReceive]'s [nextController] is set to the same instance, and it must + /// call [nextController.didAddToChannel] itself to avoid duplicate preparation. @override void didAddToChannel() { // don't call super, since nextInstanceToReceive's nextController is set to the same instance, @@ -418,14 +552,57 @@ class _ControllerRecycler extends Controller { nextInstanceToReceive?.didAddToChannel(); } + /// Delegates the documentation of API components to the next instance to receive requests. + /// + /// This method is part of the API documentation process. It calls the [documentComponents] + /// method on the [nextInstanceToReceive] if it exists, passing along the [components] + /// context. This allows the documentation to be generated for the next controller in the + /// recycling chain. + /// + /// If [nextInstanceToReceive] is null, this method does nothing. + /// + /// Parameters: + /// components: The [APIDocumentContext] used for generating API documentation. @override void documentComponents(APIDocumentContext components) => nextInstanceToReceive?.documentComponents(components); + /// Delegates the documentation of API paths to the next instance to receive requests. + /// + /// This method is part of the API documentation process. It calls the [documentPaths] + /// method on the [nextInstanceToReceive] if it exists, passing along the [components] + /// context. This allows the documentation to be generated for the next controller in the + /// recycling chain. + /// + /// If [nextInstanceToReceive] is null or its [documentPaths] returns null, an empty map is returned. + /// + /// Parameters: + /// components: The [APIDocumentContext] used for generating API documentation. + /// + /// Returns: + /// A [Map] where keys are path strings and values are [APIPath] objects, + /// or an empty map if no paths are documented. @override Map documentPaths(APIDocumentContext components) => nextInstanceToReceive?.documentPaths(components) ?? {}; + /// Delegates the documentation of API operations to the next instance to receive requests. + /// + /// This method is part of the API documentation process. It calls the [documentOperations] + /// method on the [nextInstanceToReceive] if it exists, passing along the [components], + /// [route], and [path] parameters. This allows the documentation to be generated for + /// the next controller in the recycling chain. + /// + /// If [nextInstanceToReceive] is null or its [documentOperations] returns null, an empty map is returned. + /// + /// Parameters: + /// components: The [APIDocumentContext] used for generating API documentation. + /// route: A string representing the route for which operations are being documented. + /// path: An [APIPath] object representing the path for which operations are being documented. + /// + /// Returns: + /// A [Map] where keys are operation identifiers (typically HTTP methods) and values are + /// [APIOperation] objects, or an empty map if no operations are documented. @override Map documentOperations( APIDocumentContext components, @@ -435,17 +612,49 @@ class _ControllerRecycler extends Controller { nextInstanceToReceive?.documentOperations(components, route, path) ?? {}; } +/// A controller that wraps a function to handle requests. @PreventCompilation() class _FunctionController extends Controller { _FunctionController(this._handler); + /// The function that handles requests. final FutureOr Function(Request) _handler; + /// Handles the incoming request by invoking the function controller. + /// + /// This method is the core of the _FunctionController, responsible for + /// processing incoming requests. It delegates the request handling to + /// the function (_handler) that was provided when this controller was created. + /// + /// Parameters: + /// request: The incoming [Request] object to be handled. + /// + /// Returns: + /// A [FutureOr] that resolves to a [RequestOrResponse] object or null. + /// The return value depends on the implementation of the _handler function: + /// - If it returns a [Response], that will be the result. + /// - If it returns a [Request], that request will be forwarded to the next controller. + /// - If it returns null, the request is considered handled, and no further processing occurs. @override FutureOr handle(Request request) { return _handler(request); } + /// Documents the API operations for this controller. + /// + /// This method is responsible for generating documentation for the API operations + /// associated with this controller. It delegates the documentation process to + /// the next controller in the chain, if one exists. + /// + /// Parameters: + /// - [context]: The API documentation context. + /// - [route]: The route string for the current path. + /// - [path]: The APIPath object representing the current path. + /// + /// Returns: + /// A map where the keys are operation identifiers (typically HTTP methods) + /// and the values are [APIOperation] objects describing the operations. + /// If there is no next controller, it returns an empty map. @override Map documentOperations( APIDocumentContext context, @@ -460,8 +669,11 @@ class _FunctionController extends Controller { } } +/// Abstract class representing the runtime of a controller. abstract class ControllerRuntime { + /// Whether the controller is mutable. bool get isMutable; + /// The resource controller runtime, if applicable. ResourceControllerRuntime? get resourceController; } diff --git a/packages/http/lib/src/cors_policy.dart b/packages/http/lib/src/cors_policy.dart index 07e811e..8db49cb 100644 --- a/packages/http/lib/src/cors_policy.dart +++ b/packages/http/lib/src/cors_policy.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:io'; import 'package:protevus_http/http.dart'; @@ -11,7 +20,6 @@ import 'package:protevus_http/http.dart'; /// Modifications to policy for a specific [Controller] can be accomplished in the initializer of the controller. /// /// Application-wide defaults can be managed by modifying [defaultPolicy] in a [ApplicationChannel]'s constructor. -/// class CORSPolicy { /// Create a new instance of [CORSPolicy]. /// @@ -26,6 +34,7 @@ class CORSPolicy { cacheInSeconds = def.cacheInSeconds; } + /// Creates a new instance of [CORSPolicy] with default values. CORSPolicy._defaults() { allowedOrigins = ["*"]; allowCredentials = true; @@ -50,6 +59,7 @@ class CORSPolicy { return _defaultPolicy ??= CORSPolicy._defaults(); } + /// Internal storage for the default policy. static CORSPolicy? _defaultPolicy; /// List of 'Simple' CORS headers. @@ -88,8 +98,6 @@ class CORSPolicy { /// Which response headers to expose to the client. /// /// Defaults to empty. In the specification (http://www.w3.org/TR/cors/), this is 'list of exposed headers'. - /// - /// late List exposedResponseHeaders; /// Which HTTP methods are allowed. @@ -110,6 +118,9 @@ class CORSPolicy { /// /// This will add Access-Control-Allow-Origin, Access-Control-Expose-Headers and Access-Control-Allow-Credentials /// depending on the this policy. + /// + /// [request] The incoming request. + /// Returns a map of HTTP headers. Map headersForRequest(Request request) { final origin = request.raw.headers.value("origin"); @@ -133,6 +144,9 @@ class CORSPolicy { /// Will return true if [allowedOrigins] contains the case-sensitive Origin of the [request], /// or that [allowedOrigins] contains *. /// This method is invoked internally by [Controller]s that have a [Controller.policy]. + /// + /// [request] The incoming HTTP request. + /// Returns true if the request origin is allowed, false otherwise. bool isRequestOriginAllowed(HttpRequest request) { if (allowedOrigins.contains("*")) { return true; @@ -150,6 +164,9 @@ class CORSPolicy { /// /// Will return true if the policy agrees with the Access-Control-Request-* headers of the request, otherwise, false. /// This method is invoked internally by [Controller]s that have a [Controller.policy]. + /// + /// [request] The incoming HTTP request. + /// Returns true if the preflight request is valid according to this policy, false otherwise. bool validatePreflightRequest(HttpRequest request) { if (!isRequestOriginAllowed(request)) { return false; @@ -181,6 +198,9 @@ class CORSPolicy { /// Contains the Access-Control-Allow-* headers for a CORS preflight request according /// to this policy. /// This method is invoked internally by [Controller]s that have a [Controller.policy]. + /// + /// [req] The incoming request. + /// Returns a Response object with the appropriate CORS headers. Response preflightResponse(Request req) { final headers = { "Access-Control-Allow-Origin": req.raw.headers.value("origin"), diff --git a/packages/http/lib/src/file_controller.dart b/packages/http/lib/src/file_controller.dart index 7b35826..31f74b2 100644 --- a/packages/http/lib/src/file_controller.dart +++ b/packages/http/lib/src/file_controller.dart @@ -1,11 +1,20 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:io'; - import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_http/http.dart'; import 'package:protevus_openapi/v3.dart'; import 'package:path/path.dart' as path; +/// A typedef for a function that handles file controller operations. typedef FileControllerClosure = FutureOr Function( FileController controller, Request req, @@ -48,6 +57,7 @@ class FileController extends Controller { }) : _servingDirectory = Uri.directory(pathOfDirectoryToServe), _onFileNotFound = onFileNotFound; + /// A map of default file extensions to their corresponding ContentTypes. static final Map _defaultExtensionMap = { /* Web content */ "html": ContentType("text", "html", charset: "utf-8"), @@ -80,9 +90,16 @@ class FileController extends Controller { "otf": ContentType("font", "otf"), }; + /// A map of file extensions to their corresponding ContentTypes. final Map _extensionMap = Map.from(_defaultExtensionMap); + + /// A list of policy pairs for caching. final List<_PolicyPair?> _policyPairs = []; + + /// The URI of the directory being served. final Uri _servingDirectory; + + /// A function to handle file not found errors. final FutureOr Function( FileController, Request, @@ -155,6 +172,7 @@ class FileController extends Controller { ?.policy; } + /// Handles incoming requests and serves the appropriate file. @override Future handle(Request request) async { if (request.method != "GET") { @@ -208,6 +226,7 @@ class FileController extends Controller { ..contentType = contentType; } + /// Documents the operations of this controller for API documentation. @override Map documentOperations( APIDocumentContext context, @@ -230,12 +249,21 @@ class FileController extends Controller { }; } + /// Returns the cache policy for a given file. CachePolicy? _policyForFile(File file) => cachePolicyForPath(file.path); } +/// A class to pair a cache policy with a function that determines if it should be applied. class _PolicyPair { + /// Creates a new policy pair. + /// + /// [policy] is the cache policy to apply. + /// [shouldApplyToPath] is a function that determines if the policy should be applied to a given path. _PolicyPair(this.policy, this.shouldApplyToPath); + /// A function that determines if the policy should be applied to a given path. final bool Function(String) shouldApplyToPath; + + /// The cache policy to apply. final CachePolicy policy; } diff --git a/packages/http/lib/src/handler_exception.dart b/packages/http/lib/src/handler_exception.dart index 492d01e..d7d2576 100644 --- a/packages/http/lib/src/handler_exception.dart +++ b/packages/http/lib/src/handler_exception.dart @@ -1,9 +1,31 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_http/http.dart'; +/// A custom exception class for handling HTTP-related errors. +/// +/// This exception is typically thrown when an HTTP handler encounters an error +/// and needs to provide a specific [Response] object as part of the exception. class HandlerException implements Exception { + /// Constructs a [HandlerException] with the given [Response]. + /// + /// @param _response The HTTP response associated with this exception. HandlerException(this._response); + /// Gets the [Response] object associated with this exception. + /// + /// This getter provides read-only access to the internal [_response] field. + /// + /// @return The [Response] object containing details about the HTTP error. Response get response => _response; + /// The private field storing the HTTP response associated with this exception. final Response _response; } diff --git a/packages/http/lib/src/http_codec_repository.dart b/packages/http/lib/src/http_codec_repository.dart index f19f07f..1f348e0 100644 --- a/packages/http/lib/src/http_codec_repository.dart +++ b/packages/http/lib/src/http_codec_repository.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:convert'; import 'dart:io'; import 'package:protevus_http/http.dart'; @@ -11,6 +20,7 @@ import 'package:protevus_http/http.dart'; /// Additional mappings are added via [add]. This method must be called per-isolate and it is recommended /// to add mappings in an application's [ApplicationChannel] subclass constructor. class CodecRegistry { + /// Private constructor to prevent direct instantiation. CodecRegistry._() { add( ContentType("application", "json", charset: "utf-8"), @@ -31,10 +41,19 @@ class CodecRegistry { static CodecRegistry get defaultInstance => _defaultInstance; static final CodecRegistry _defaultInstance = CodecRegistry._(); + /// Map of primary content types to their respective codecs. final Map _primaryTypeCodecs = {}; + + /// Map of fully specified content types to their respective codecs. final Map> _fullySpecificedCodecs = {}; + + /// Map of primary content types to their compression settings. final Map _primaryTypeCompressionMap = {}; + + /// Map of fully specified content types to their compression settings. final Map> _fullySpecifiedCompressionMap = {}; + + /// Map of content types to their default charsets. final Map> _defaultCharsetMap = {}; /// Adds a custom [codec] for [contentType]. @@ -167,6 +186,7 @@ class CodecRegistry { return null; } + /// Returns a [Codec] for the given [charset]. Codec> _codecForCharset(String? charset) { final encoding = Encoding.getByName(charset); if (encoding == null) { @@ -176,6 +196,7 @@ class CodecRegistry { return encoding; } + /// Returns the default charset [Codec] for the given [ContentType]. Codec>? _defaultCharsetCodecForType(ContentType type) { final inner = _defaultCharsetMap[type.primaryType]; if (inner == null) { @@ -191,6 +212,7 @@ class CodecRegistry { } } +/// A [Codec] for encoding and decoding form data. class _FormCodec extends Codec?, dynamic> { const _FormCodec(); @@ -201,6 +223,7 @@ class _FormCodec extends Codec?, dynamic> { Converter> get decoder => const _FormDecoder(); } +/// A [Converter] for encoding form data. class _FormEncoder extends Converter, String> { const _FormEncoder(); @@ -209,6 +232,7 @@ class _FormEncoder extends Converter, String> { return data.keys.map((k) => _encodePair(k, data[k])).join("&"); } + /// Encodes a key-value pair for form data. String _encodePair(String key, dynamic value) { String encode(String v) => "$key=${Uri.encodeQueryComponent(v)}"; if (value is List) { @@ -223,6 +247,7 @@ class _FormEncoder extends Converter, String> { } } +/// A [Converter] for decoding form data. class _FormDecoder extends Converter> { // This class may take input as either String or List. If charset is not defined in request, // then data is List (from CodecRegistry) and will default to being UTF8 decoded first. @@ -230,29 +255,81 @@ class _FormDecoder extends Converter> { const _FormDecoder(); + /// Converts a URL-encoded form data string into a Map of key-value pairs. + /// + /// This method takes a [String] `data` containing URL-encoded form data + /// and returns a [Map] where the keys are the form field names + /// and the values are either a single String or a List for multiple values. + /// + /// The conversion is performed by creating a [Uri] object with the input data + /// as its query string, then accessing its [Uri.queryParametersAll] property. + /// + /// Example: + /// Input: "name=John&age=30&hobby=reading&hobby=gaming" + /// Output: { + /// "name": ["John"], + /// "age": ["30"], + /// "hobby": ["reading", "gaming"] + /// } @override Map convert(String data) { return Uri(query: data).queryParametersAll; } + /// Starts a chunked conversion process for form data. + /// + /// This method initializes and returns a [_FormSink] object, which is used to + /// handle the chunked conversion of form data. The [_FormSink] accumulates + /// incoming data chunks and performs the final conversion when the data stream + /// is closed. + /// + /// [outSink] is the output sink where the converted Map will + /// be added after the conversion is complete. + /// + /// Returns a [_FormSink] object that can be used to add string chunks of form + /// data for conversion. @override _FormSink startChunkedConversion(Sink> outSink) { return _FormSink(outSink); } } +/// A [ChunkedConversionSink] for form data. class _FormSink implements ChunkedConversionSink { _FormSink(this._outSink); + /// The decoder used to convert the form data. final _FormDecoder decoder = const _FormDecoder(); + + /// The output sink for the converted data. final Sink> _outSink; + + /// Buffer to accumulate incoming data. final StringBuffer _buffer = StringBuffer(); + /// Adds a chunk of form data to the buffer. + /// + /// This method is part of the chunked conversion process for form data. + /// It appends the given [data] string to an internal buffer, which will be + /// processed when the [close] method is called. + /// + /// [data] is a String containing a portion of the form data to be converted. @override void add(String data) { _buffer.write(data); } + /// Completes the chunked conversion process for form data. + /// + /// This method is called when all chunks of the form data have been added + /// to the buffer. It performs the following steps: + /// 1. Converts the accumulated buffer content to a string. + /// 2. Uses the decoder to convert this string into a Map. + /// 3. Adds the resulting map to the output sink. + /// 4. Closes the output sink. + /// + /// This method should be called after all add() operations are complete + /// to finalize the conversion process and clean up resources. @override void close() { _outSink.add(decoder.convert(_buffer.toString())); diff --git a/packages/http/lib/src/managed_object_controller.dart b/packages/http/lib/src/managed_object_controller.dart index f24ade8..9672075 100644 --- a/packages/http/lib/src/managed_object_controller.dart +++ b/packages/http/lib/src/managed_object_controller.dart @@ -1,5 +1,13 @@ -import 'dart:async'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:async'; import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/db.dart'; import 'package:protevus_http/http.dart'; @@ -40,6 +48,8 @@ import 'package:protevus_openapi/v3.dart'; class ManagedObjectController extends ResourceController { /// Creates an instance of a [ManagedObjectController]. + /// + /// [context] is the [ManagedContext] used for database operations. ManagedObjectController(ManagedContext context) : super() { _query = Query(context); } @@ -49,6 +59,8 @@ class ManagedObjectController /// This method is used when generating instances of this type dynamically from runtime values, /// where the static type argument cannot be defined. Behaves just like the unnamed constructor. /// + /// [entity] is the [ManagedEntity] for the object type being controlled. + /// [context] is the [ManagedContext] used for database operations. ManagedObjectController.forEntity( ManagedEntity entity, ManagedContext context, @@ -59,10 +71,13 @@ class ManagedObjectController /// Returns a route pattern for using [ManagedObjectController]s. /// /// Returns the string "/$name/[:id]", to be used as a route pattern in a [Router] for instances of [ResourceController] and subclasses. + /// + /// [name] is the name to be used in the route pattern. static String routePattern(String name) { return "/$name/[:id]"; } + /// The query used for database operations. Query? _query; /// Executed prior to a fetch by ID query. @@ -92,6 +107,9 @@ class ManagedObjectController return Response.notFound(); } + /// Handles GET requests for a single object by ID. + /// + /// [id] is the ID of the object to fetch. @Operation.get("id") Future getObject(@Bind.path("id") String id) async { final primaryKey = _query!.entity.primaryKey; @@ -128,6 +146,7 @@ class ManagedObjectController return Response.ok(object); } + /// Handles POST requests to create a new object. @Operation.post() Future createObject() async { final instance = _query!.entity.instanceOf() as InstanceType; @@ -165,6 +184,9 @@ class ManagedObjectController return Response.notFound(); } + /// Handles DELETE requests to delete an object by ID. + /// + /// [id] is the ID of the object to delete. @Operation.delete("id") Future deleteObject(@Bind.path("id") String id) async { final primaryKey = _query!.entity.primaryKey; @@ -208,6 +230,9 @@ class ManagedObjectController return Response.notFound(); } + /// Handles PUT requests to update an object by ID. + /// + /// [id] is the ID of the object to update. @Operation.put("id") Future updateObject(@Bind.path("id") String id) async { final primaryKey = _query!.entity.primaryKey; @@ -247,6 +272,9 @@ class ManagedObjectController return Response.ok(objects); } + /// Handles GET requests to fetch multiple objects. + /// + /// Supports pagination, sorting, and filtering through query parameters. @Operation.get() Future getObjects({ /// Limits the number of objects returned. @@ -359,6 +387,7 @@ class ManagedObjectController return didFindObjects(results); } + /// Documents the request body for POST and PUT operations. @override APIRequestBody? documentOperationRequestBody( APIDocumentContext context, @@ -375,6 +404,7 @@ class ManagedObjectController return null; } + /// Documents the responses for each operation type. @override Map documentOperationResponses( APIDocumentContext context, @@ -445,6 +475,7 @@ class ManagedObjectController return {}; } + /// Documents the operations for this controller. @override Map documentOperations( APIDocumentContext context, @@ -469,6 +500,10 @@ class ManagedObjectController return ops; } + /// Parses the identifier from the path. + /// + /// [value] is the string value from the path. + /// [desc] is the property description for the identifier. dynamic _getIdentifierFromPath( String value, ManagedPropertyDescription? desc, @@ -476,6 +511,11 @@ class ManagedObjectController return _parseValueForProperty(value, desc, onError: Response.notFound()); } + /// Parses a value for a specific property. + /// + /// [value] is the string value to parse. + /// [desc] is the property description. + /// [onError] is the response to return if parsing fails. dynamic _parseValueForProperty( String value, ManagedPropertyDescription? desc, { diff --git a/packages/http/lib/src/query_controller.dart b/packages/http/lib/src/query_controller.dart index 4bbc9d8..40f1183 100644 --- a/packages/http/lib/src/query_controller.dart +++ b/packages/http/lib/src/query_controller.dart @@ -1,5 +1,13 @@ -import 'dart:async'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:async'; import 'package:protevus_database/db.dart'; import 'package:protevus_http/http.dart'; @@ -20,6 +28,8 @@ import 'package:protevus_http/http.dart'; abstract class QueryController extends ResourceController { /// Create an instance of [QueryController]. + /// + /// [context] is the [ManagedContext] used for database operations. QueryController(ManagedContext context) : super() { query = Query(context); } @@ -34,6 +44,12 @@ abstract class QueryController /// 3. If the [Request] contains a body, it will be decoded per the [acceptedContentTypes] and deserialized into the [Query.values] property via [ManagedObject.readFromMap]. Query? query; + /// Overrides [ResourceController.willProcessRequest] to set up the [query] based on the request. + /// + /// This method checks if there's a path variable matching the primary key of [InstanceType], + /// and if so, sets up the [query] to filter by this primary key value. + /// + /// Returns a [Future] that completes with either the [Request] or a [Response]. @override FutureOr willProcessRequest(Request req) { if (req.path.orderedVariableNames.isNotEmpty) { @@ -64,6 +80,12 @@ abstract class QueryController return super.willProcessRequest(req); } + /// Overrides [ResourceController.didDecodeRequestBody] to populate [query.values] with the decoded request body. + /// + /// This method reads the decoded request body into [query.values] and removes the primary key + /// from the backing map to prevent accidental updates to the primary key. + /// + /// [body] is the decoded request body. @override void didDecodeRequestBody(RequestBody body) { query!.values.readFromMap(body.as()); diff --git a/packages/http/lib/src/request.dart b/packages/http/lib/src/request.dart index 8957af3..ec8a690 100644 --- a/packages/http/lib/src/request.dart +++ b/packages/http/lib/src/request.dart @@ -1,7 +1,15 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:convert'; import 'dart:io'; - import 'package:protevus_auth/auth.dart'; import 'package:protevus_http/http.dart'; @@ -73,6 +81,7 @@ class Request implements RequestOrResponse { /// null if no permission has been set. Authorization? authorization; + /// List of response modifiers to be applied before sending the response. List? _responseModifiers; /// The acceptable content types for a [Response] returned for this instance. @@ -125,6 +134,7 @@ class Request implements RequestOrResponse { return _cachedAcceptableTypes!; } + /// Cached list of acceptable content types. List? _cachedAcceptableTypes; /// Whether a [Response] may contain a body of type [contentType]. @@ -205,6 +215,7 @@ class Request implements RequestOrResponse { _responseModifiers!.add(modifier); } + /// Returns a sanitized version of the request headers as a string. String get _sanitizedHeaders { final StringBuffer buf = StringBuffer("{"); @@ -216,6 +227,7 @@ class Request implements RequestOrResponse { return buf.toString(); } + /// Truncates a string to a specified length, adding an ellipsis if truncated. String _truncatedString(String originalString, {int charSize = 128}) { if (originalString.length <= charSize) { return originalString; @@ -302,6 +314,7 @@ class Request implements RequestOrResponse { throw StateError("Invalid response body. Could not encode."); } + /// Encodes the response body as bytes, applying compression if necessary. List? _responseBodyBytes( Response resp, _Reference compressionType, @@ -346,6 +359,7 @@ class Request implements RequestOrResponse { return codec.encode(resp.body); } + /// Encodes the response body as a stream, applying compression if necessary. Stream> _responseBodyStream( Response resp, _Reference compressionType, @@ -383,6 +397,7 @@ class Request implements RequestOrResponse { return codec.encoder.bind(resp.body as Stream); } + /// Whether the client accepts gzip-encoded response bodies. bool get _acceptsGzipResponseBody { return raw.headers[HttpHeaders.acceptEncodingHeader] ?.any((v) => v.split(",").any((s) => s.trim() == "gzip")) ?? @@ -434,15 +449,23 @@ class Request implements RequestOrResponse { } } +/// Exception thrown when there's an error during HTTP streaming. class HTTPStreamingException implements Exception { + /// Creates a new [HTTPStreamingException] with the given underlying exception and stack trace. HTTPStreamingException(this.underlyingException, this.trace); + /// The underlying exception that caused the streaming error. dynamic underlyingException; + + /// The stack trace associated with the underlying exception. StackTrace trace; } +/// A reference wrapper class for holding mutable values. class _Reference { + /// Creates a new [_Reference] with the given initial value. _Reference(this.value); + /// The wrapped value. T? value; } diff --git a/packages/http/lib/src/request_body.dart b/packages/http/lib/src/request_body.dart index 2fec60e..9e68a20 100644 --- a/packages/http/lib/src/request_body.dart +++ b/packages/http/lib/src/request_body.dart @@ -1,6 +1,14 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:io'; - import 'package:protevus_http/http.dart'; /// Objects that represent a request body, and can be decoded into Dart objects. @@ -17,6 +25,8 @@ class RequestBody extends BodyDecoder { /// See [CodecRegistry] for more information about how data is decoded. /// /// Decoded data is cached the after it is decoded. + /// + /// [request] The HttpRequest object to be decoded. RequestBody(HttpRequest super.request) : _request = request, _originalByteStream = request; @@ -26,13 +36,26 @@ class RequestBody extends BodyDecoder { /// A request with a body larger than this size will be rejected. Value is in bytes. Defaults to 10MB (1024 * 1024 * 10). static int maxSize = 1024 * 1024 * 10; + /// The original HttpRequest object. final HttpRequest _request; + /// Checks if the request has content. + /// + /// Returns true if the request has a content length or uses chunked transfer encoding. bool get _hasContent => _hasContentLength || _request.headers.chunkedTransferEncoding; + /// Checks if the request has a content length. + /// + /// Returns true if the request has a content length greater than 0. bool get _hasContentLength => (_request.headers.contentLength) > 0; + /// Gets the byte stream of the request body. + /// + /// If the content length is specified and doesn't exceed [maxSize], returns the original stream. + /// Otherwise, buffers the stream and checks for size limits. + /// + /// Throws a [Response] with status 413 if the body size exceeds [maxSize]. @override Stream> get bytes { // If content-length is specified, then we can check it for maxSize @@ -88,18 +111,32 @@ class RequestBody extends BodyDecoder { return _bufferingController!.stream; } + /// Gets the content type of the request. + /// + /// Returns null if no content type is specified. @override ContentType? get contentType => _request.headers.contentType; + /// Checks if the request body is empty. + /// + /// Returns true if the request has no content. @override bool get isEmpty => !_hasContent; + /// Checks if the request body is form data. + /// + /// Returns true if the content type is "application/x-www-form-urlencoded". bool get isFormData => contentType != null && contentType!.primaryType == "application" && contentType!.subType == "x-www-form-urlencoded"; + /// The original byte stream of the request. final Stream> _originalByteStream; + + /// A buffering controller for the byte stream when content length is not specified. StreamController>? _bufferingController; + + /// The number of bytes read from the request body. int _bytesRead = 0; } diff --git a/packages/http/lib/src/request_path.dart b/packages/http/lib/src/request_path.dart index 893c654..8104224 100644 --- a/packages/http/lib/src/request_path.dart +++ b/packages/http/lib/src/request_path.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_http/http.dart'; /// Stores path info for a [Request]. @@ -10,8 +19,14 @@ class RequestPath { /// Default constructor for [RequestPath]. /// /// There is no need to invoke this constructor manually. + /// + /// [segments] is a list of path segments. RequestPath(this.segments); + /// Sets the route specification for this request path. + /// + /// [spec] is the [RouteSpecification] to set. + /// [segmentOffset] is the offset to start processing segments from (default is 0). void setSpecification(RouteSpecification spec, {int segmentOffset = 0}) { final requestIterator = segments.iterator; for (var i = 0; i < segmentOffset; i++) { @@ -50,7 +65,6 @@ class RequestPath { /// Consider a match specification /users/:id. If the evaluated path is /// /users/2 /// This property will be {'id' : '2'}. - /// Map variables = {}; /// A list of the segments in a matched path. diff --git a/packages/http/lib/src/resource_controller.dart b/packages/http/lib/src/resource_controller.dart index 0dc071c..0df6dfc 100755 --- a/packages/http/lib/src/resource_controller.dart +++ b/packages/http/lib/src/resource_controller.dart @@ -1,7 +1,15 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:ffi'; import 'dart:io'; - import 'package:collection/collection.dart' show IterableExtension; import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_auth/auth.dart'; @@ -67,15 +75,18 @@ import 'package:meta/meta.dart'; /// To access the request directly, use [request]. Note that the [Request.body] of [request] will be decoded prior to invoking an operation method. abstract class ResourceController extends Controller implements Recyclable { + /// Constructor for ResourceController. ResourceController() { _runtime = (RuntimeContext.current.runtimes[runtimeType] as ControllerRuntime?) ?.resourceController; } + /// Getter for the recycled state of the controller. @override void get recycledState => nullptr; + /// The runtime for this ResourceController. ResourceControllerRuntime? _runtime; /// The request being processed by this [ResourceController]. @@ -129,11 +140,13 @@ abstract class ResourceController extends Controller /// this method is not called. void didDecodeRequestBody(RequestBody body) {} + /// Restores the state of the controller. @override void restore(void state) { /* no op - fetched from static cache in Runtime */ } + /// Handles the incoming request. @override FutureOr handle(Request request) async { this.request = request; @@ -226,6 +239,7 @@ abstract class ResourceController extends Controller return [tag]; } + /// Documents the operations for this controller. @override Map documentOperations( APIDocumentContext context, @@ -235,11 +249,13 @@ abstract class ResourceController extends Controller return _runtime!.documenter!.documentOperations(this, context, route, path); } + /// Documents the components for this controller. @override void documentComponents(APIDocumentContext context) { _runtime!.documenter?.documentComponents(this, context); } + /// Checks if the request content type is supported. bool _requestContentTypeIsSupported(Request? req) { final incomingContentType = request!.raw.headers.contentType; return acceptedContentTypes.firstWhereOrNull((ct) { @@ -249,6 +265,7 @@ abstract class ResourceController extends Controller null; } + /// Returns a list of allowed HTTP methods for the given path variables. List _allowedMethodsForPathVariables( Iterable pathVariables, ) { @@ -258,6 +275,7 @@ abstract class ResourceController extends Controller .toList(); } + /// Processes the request and returns a response. Future _process() async { if (!request!.body.isEmpty) { if (!_requestContentTypeIsSupported(request)) { diff --git a/packages/http/lib/src/resource_controller_bindings.dart b/packages/http/lib/src/resource_controller_bindings.dart index 70cb393..bb90062 100644 --- a/packages/http/lib/src/resource_controller_bindings.dart +++ b/packages/http/lib/src/resource_controller_bindings.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_http/http.dart'; /// Binds an instance method in [ResourceController] to an operation. @@ -14,6 +23,7 @@ import 'package:protevus_http/http.dart'; /// } /// } class Operation { + /// Creates an [Operation] with the specified [method] and optional path variables. const Operation( this.method, [ String? pathVariable1, @@ -25,6 +35,7 @@ class Operation { _pathVariable3 = pathVariable3, _pathVariable4 = pathVariable4; + /// Creates a GET [Operation] with optional path variables. const Operation.get([ String? pathVariable1, String? pathVariable2, @@ -36,6 +47,7 @@ class Operation { _pathVariable3 = pathVariable3, _pathVariable4 = pathVariable4; + /// Creates a PUT [Operation] with optional path variables. const Operation.put([ String? pathVariable1, String? pathVariable2, @@ -47,6 +59,7 @@ class Operation { _pathVariable3 = pathVariable3, _pathVariable4 = pathVariable4; + /// Creates a POST [Operation] with optional path variables. const Operation.post([ String? pathVariable1, String? pathVariable2, @@ -58,6 +71,7 @@ class Operation { _pathVariable3 = pathVariable3, _pathVariable4 = pathVariable4; + /// Creates a DELETE [Operation] with optional path variables. const Operation.delete([ String? pathVariable1, String? pathVariable2, @@ -69,10 +83,19 @@ class Operation { _pathVariable3 = pathVariable3, _pathVariable4 = pathVariable4; + /// The HTTP method for this operation. final String method; + + /// The first path variable (if any). final String? _pathVariable1; + + /// The second path variable (if any). final String? _pathVariable2; + + /// The third path variable (if any). final String? _pathVariable3; + + /// The fourth path variable (if any). final String? _pathVariable4; /// Returns a list of all path variables required for this operation. @@ -209,15 +232,26 @@ class Bind { ignore = null, reject = null; + /// The name of the binding (for query, header, and path bindings). final String? name; + + /// The type of binding (query, header, body, or path). final BindingType bindingType; + /// List of keys to accept in the request body (for body bindings). final List? accept; + + /// List of keys to ignore in the request body (for body bindings). final List? ignore; + + /// List of keys to reject in the request body (for body bindings). final List? reject; + + /// List of keys required in the request body (for body bindings). final List? require; } +/// Enum representing the types of bindings available. enum BindingType { query, header, body, path } /// Marks an [ResourceController] property binding as required. @@ -245,7 +279,10 @@ enum BindingType { query, header, body, path } /// } const RequiredBinding requiredBinding = RequiredBinding(); -/// See [requiredBinding]. +/// Class representing a required binding. +/// +/// See [requiredBinding] for more information. class RequiredBinding { + /// Creates a [RequiredBinding] instance. const RequiredBinding(); } diff --git a/packages/http/lib/src/resource_controller_interfaces.dart b/packages/http/lib/src/resource_controller_interfaces.dart index 3b9759c..352ea10 100755 --- a/packages/http/lib/src/resource_controller_interfaces.dart +++ b/packages/http/lib/src/resource_controller_interfaces.dart @@ -1,17 +1,36 @@ -import 'dart:async'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:async'; import 'package:collection/collection.dart' show IterableExtension; import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_auth/auth.dart'; import 'package:protevus_http/http.dart'; import 'package:protevus_openapi/v3.dart'; +/// Abstract class representing the runtime of a ResourceController. abstract class ResourceControllerRuntime { + /// List of instance variable parameters. List? ivarParameters; + + /// List of operations supported by the ResourceController. late List operations; + /// Documenter for the ResourceController. ResourceControllerDocumenter? documenter; + /// Retrieves the operation runtime for a given method and path variables. + /// + /// [method] The HTTP method. + /// [pathVariables] The list of path variables. + /// + /// Returns the matching [ResourceControllerOperation] or null if not found. ResourceControllerOperation? getOperationRuntime( String method, List pathVariables, @@ -21,27 +40,58 @@ abstract class ResourceControllerRuntime { ); } + /// Applies request properties to the controller. + /// + /// [untypedController] The ResourceController instance. + /// [args] The invocation arguments. void applyRequestProperties( ResourceController untypedController, ResourceControllerOperationInvocationArgs args, ); } +/// Abstract class for documenting a ResourceController. abstract class ResourceControllerDocumenter { + /// Documents the components of a ResourceController. + /// + /// [rc] The ResourceController instance. + /// [context] The API documentation context. void documentComponents(ResourceController rc, APIDocumentContext context); + /// Documents the operation parameters of a ResourceController. + /// + /// [rc] The ResourceController instance. + /// [context] The API documentation context. + /// [operation] The operation to document. + /// + /// Returns a list of [APIParameter] objects. List documentOperationParameters( ResourceController rc, APIDocumentContext context, Operation? operation, ); + /// Documents the operation request body of a ResourceController. + /// + /// [rc] The ResourceController instance. + /// [context] The API documentation context. + /// [operation] The operation to document. + /// + /// Returns an [APIRequestBody] object or null. APIRequestBody? documentOperationRequestBody( ResourceController rc, APIDocumentContext context, Operation? operation, ); + /// Documents the operations of a ResourceController. + /// + /// [rc] The ResourceController instance. + /// [context] The API documentation context. + /// [route] The route string. + /// [path] The API path. + /// + /// Returns a map of operation names to [APIOperation] objects. Map documentOperations( ResourceController rc, APIDocumentContext context, @@ -50,7 +100,9 @@ abstract class ResourceControllerDocumenter { ); } +/// Represents an operation in a ResourceController. class ResourceControllerOperation { + /// Creates a new ResourceControllerOperation. ResourceControllerOperation({ required this.scopes, required this.pathVariables, @@ -61,23 +113,36 @@ class ResourceControllerOperation { required this.invoker, }); + /// The required authentication scopes for this operation. final List? scopes; + + /// The path variables for this operation. final List pathVariables; + + /// The HTTP method for this operation. final String httpMethod; + + /// The name of the Dart method implementing this operation. final String dartMethodName; + /// The positional parameters for this operation. final List positionalParameters; + + /// The named parameters for this operation. final List namedParameters; + /// The function to invoke this operation. final Future Function( ResourceController resourceController, ResourceControllerOperationInvocationArgs args, ) invoker; - /// Checks if a request's method and path variables will select this binder. + /// Checks if a request's method and path variables will select this operation. /// - /// Note that [requestMethod] may be null; if this is the case, only - /// path variables are compared. + /// [requestMethod] The HTTP method of the request. + /// [requestPathVariables] The path variables of the request. + /// + /// Returns true if the operation is suitable for the request, false otherwise. bool isSuitableForRequest( String? requestMethod, List requestPathVariables, @@ -94,7 +159,9 @@ class ResourceControllerOperation { } } +/// Represents a parameter in a ResourceController operation. class ResourceControllerParameter { + /// Creates a new ResourceControllerParameter. ResourceControllerParameter({ required this.symbolName, required this.name, @@ -109,6 +176,7 @@ class ResourceControllerParameter { required this.rejectFilter, }) : _decoder = decoder; + /// Creates a typed ResourceControllerParameter. static ResourceControllerParameter make({ required String symbolName, required String? name, @@ -136,22 +204,40 @@ class ResourceControllerParameter { ); } + /// The name of the symbol in the Dart code. final String symbolName; + + /// The name of the parameter in the API. final String? name; + + /// The type of the parameter. final Type type; + + /// The default value of the parameter. final dynamic defaultValue; + + /// The filter for accepted values. final List? acceptFilter; + + /// The filter for ignored values. final List? ignoreFilter; + + /// The filter for required values. final List? requireFilter; + + /// The filter for rejected values. final List? rejectFilter; - /// The location in the request that this parameter is bound to + /// The location of the parameter in the request. final BindingType location; + /// Indicates if the parameter is required. final bool isRequired; + /// The decoder function for the parameter. final dynamic Function(dynamic input)? _decoder; + /// Gets the API parameter location for this parameter. APIParameterLocation get apiLocation { switch (location) { case BindingType.body: @@ -165,6 +251,7 @@ class ResourceControllerParameter { } } + /// Gets the location name as a string. String get locationName { switch (location) { case BindingType.query: @@ -178,6 +265,11 @@ class ResourceControllerParameter { } } + /// Decodes the parameter value from the request. + /// + /// [request] The HTTP request. + /// + /// Returns the decoded value. dynamic decode(Request? request) { switch (location) { case BindingType.query: @@ -220,8 +312,14 @@ class ResourceControllerParameter { } } +/// Holds the arguments for invoking a ResourceController operation. class ResourceControllerOperationInvocationArgs { + /// The instance variables for the invocation. late Map instanceVariables; + + /// The named arguments for the invocation. late Map namedArguments; + + /// The positional arguments for the invocation. late List positionalArguments; } diff --git a/packages/http/lib/src/resource_controller_scope.dart b/packages/http/lib/src/resource_controller_scope.dart index 4100f64..a77589d 100644 --- a/packages/http/lib/src/resource_controller_scope.dart +++ b/packages/http/lib/src/resource_controller_scope.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_auth/auth.dart'; import 'package:protevus_http/http.dart'; @@ -33,11 +42,16 @@ import 'package:protevus_http/http.dart'; /// .link(() => Authorizer.bearer(authServer)) /// .link(() => NoteController()); class Scope { - /// Add to [ResourceController] operation method to require authorization scope. + /// Constructor for the Scope class. /// - /// An incoming [Request.authorization] must have sufficient scope for all [scopes]. + /// Creates a new Scope instance with the provided list of scopes. + /// + /// [scopes] is the list of authorization scopes required. const Scope(this.scopes); /// The list of authorization scopes required. + /// + /// This list contains the string representations of the scopes that are + /// required for the annotated operation method. final List scopes; } diff --git a/packages/http/lib/src/response.dart b/packages/http/lib/src/response.dart index 56028de..6f13d78 100644 --- a/packages/http/lib/src/response.dart +++ b/packages/http/lib/src/response.dart @@ -1,8 +1,16 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; - import 'package:protevus_http/http.dart'; /// Represents the information in an HTTP response. @@ -14,6 +22,10 @@ class Response implements RequestOrResponse { /// /// There exist convenience constructors for common response status codes /// and you should prefer to use those. + /// + /// [statusCode] The HTTP status code for this response. + /// [headers] A map of HTTP headers for this response. + /// [body] The body content of this response. Response(int this.statusCode, Map? headers, dynamic body) { this.body = body; this.headers = LinkedHashMap( @@ -22,13 +34,18 @@ class Response implements RequestOrResponse { this.headers.addAll(headers ?? {}); } - /// Represents a 200 response. + /// Represents a 200 OK response. + /// + /// [body] The body content of this response. + /// [headers] Optional map of HTTP headers for this response. Response.ok(dynamic body, {Map? headers}) : this(HttpStatus.ok, headers, body); - /// Represents a 201 response. + /// Represents a 201 Created response. /// - /// The [location] is a URI that is added as the Location header. + /// [location] A URI that is added as the Location header. + /// [body] Optional body content of this response. + /// [headers] Optional map of HTTP headers for this response. Response.created( String location, { dynamic body, @@ -39,48 +56,73 @@ class Response implements RequestOrResponse { body, ); - /// Represents a 202 response. + /// Represents a 202 Accepted response. + /// + /// [headers] Optional map of HTTP headers for this response. Response.accepted({Map? headers}) : this(HttpStatus.accepted, headers, null); - /// Represents a 204 response. + /// Represents a 204 No Content response. + /// + /// [headers] Optional map of HTTP headers for this response. Response.noContent({Map? headers}) : this(HttpStatus.noContent, headers, null); - /// Represents a 304 response. + /// Represents a 304 Not Modified response. /// - /// Where [lastModified] is the last modified date of the resource - /// and [cachePolicy] is the same policy as applied when this resource was first fetched. + /// [lastModified] The last modified date of the resource. + /// [cachePolicy] The same policy as applied when this resource was first fetched. Response.notModified(DateTime lastModified, this.cachePolicy) { statusCode = HttpStatus.notModified; headers = {HttpHeaders.lastModifiedHeader: HttpDate.format(lastModified)}; } - /// Represents a 400 response. + /// Represents a 400 Bad Request response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.badRequest({Map? headers, dynamic body}) : this(HttpStatus.badRequest, headers, body); - /// Represents a 401 response. + /// Represents a 401 Unauthorized response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.unauthorized({Map? headers, dynamic body}) : this(HttpStatus.unauthorized, headers, body); - /// Represents a 403 response. + /// Represents a 403 Forbidden response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.forbidden({Map? headers, dynamic body}) : this(HttpStatus.forbidden, headers, body); - /// Represents a 404 response. + /// Represents a 404 Not Found response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.notFound({Map? headers, dynamic body}) : this(HttpStatus.notFound, headers, body); - /// Represents a 409 response. + /// Represents a 409 Conflict response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.conflict({Map? headers, dynamic body}) : this(HttpStatus.conflict, headers, body); - /// Represents a 410 response. + /// Represents a 410 Gone response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.gone({Map? headers, dynamic body}) : this(HttpStatus.gone, headers, body); - /// Represents a 500 response. + /// Represents a 500 Internal Server Error response. + /// + /// [headers] Optional map of HTTP headers for this response. + /// [body] Optional body content of this response. Response.serverError({Map? headers, dynamic body}) : this(HttpStatus.internalServerError, headers, body); @@ -112,6 +154,12 @@ class Response implements RequestOrResponse { _body = serializedBody ?? initialResponseBody; } + /// The internal storage for the response body. + /// + /// This private variable holds the actual content of the response body. + /// It can be of any type (dynamic) to accommodate various types of response data. + /// The public 'body' getter and setter methods interact with this variable + /// to provide controlled access and manipulation of the response body. dynamic _body; /// Whether or not this instance should buffer its output or send it right away. @@ -135,11 +183,22 @@ class Response implements RequestOrResponse { /// /// See [contentType] for behavior when setting 'content-type' in this property. Map get headers => _headers; + + /// Sets the headers for this response. + /// + /// Clears existing headers and adds all headers from the provided map. set headers(Map h) { _headers.clear(); _headers.addAll(h); } + /// A case-insensitive map for storing HTTP headers. + /// + /// This map uses a custom equality and hash function to ensure that header names + /// are treated case-insensitively. For example, 'Content-Type' and 'content-type' + /// are considered the same key. + /// + /// The map is implemented as a [LinkedHashMap] to maintain the order of insertion. final Map _headers = LinkedHashMap( equals: (a, b) => a.toLowerCase() == b.toLowerCase(), hashCode: (key) => key.toLowerCase().hashCode); @@ -188,13 +247,14 @@ class Response implements RequestOrResponse { ); } + /// Sets the content type for this response. set contentType(ContentType? t) { _contentType = t; } ContentType? _contentType; - /// Whether or nor this instance has explicitly has its [contentType] property. + /// Whether or not this instance has explicitly set its [contentType] property. /// /// This value indicates whether or not [contentType] has been set, or is still using its default value. bool get hasExplicitlySetContentType => _contentType != null; @@ -209,6 +269,11 @@ class Response implements RequestOrResponse { /// from disk where it is already stored as an encoded list of bytes. bool encodeBody = true; + /// Combines two header maps into a single map. + /// + /// [inputHeaders] The initial set of headers. + /// [otherHeaders] Additional headers to be added. + /// Returns a new map containing all headers from both input maps. static Map _headersWith( Map? inputHeaders, Map otherHeaders, diff --git a/packages/http/lib/src/route_node.dart b/packages/http/lib/src/route_node.dart index b79f431..e6e5726 100644 --- a/packages/http/lib/src/route_node.dart +++ b/packages/http/lib/src/route_node.dart @@ -1,6 +1,19 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_http/http.dart'; +/// Represents a segment of a route path. class RouteSegment { + /// Creates a new RouteSegment from a string segment. + /// + /// [segment] The string representation of the route segment. RouteSegment(String segment) { if (segment == "*") { isRemainingMatcher = true; @@ -22,6 +35,12 @@ class RouteSegment { } } + /// Creates a new RouteSegment directly with specified properties. + /// + /// [literal] The literal string of the segment. + /// [variableName] The name of the variable if this is a variable segment. + /// [expression] The regular expression string for matching. + /// [matchesAnything] Whether this segment matches anything (like "*"). RouteSegment.direct({ this.literal, this.variableName, @@ -34,18 +53,34 @@ class RouteSegment { } } + /// The literal string of the segment. String? literal; + + /// The name of the variable if this is a variable segment. String? variableName; + + /// The regular expression for matching this segment. RegExp? matcher; + /// Whether this segment is a literal matcher. bool get isLiteralMatcher => !isRemainingMatcher && !isVariable && !hasRegularExpression; + /// Whether this segment has a regular expression for matching. bool get hasRegularExpression => matcher != null; + /// Whether this segment is a variable. bool get isVariable => variableName != null; + + /// Whether this segment matches all remaining segments. bool isRemainingMatcher = false; + /// Checks if this RouteSegment is equal to another object. + /// + /// Returns true if the [other] object is a RouteSegment and has the same + /// [literal], [variableName], [isRemainingMatcher], and [matcher] pattern. + /// + /// [other] The object to compare with this RouteSegment. @override bool operator ==(Object other) => other is RouteSegment && @@ -54,9 +89,25 @@ class RouteSegment { isRemainingMatcher == other.isRemainingMatcher && matcher?.pattern == other.matcher?.pattern; + /// Generates a hash code for this RouteSegment. + /// + /// The hash code is based on either the [literal] value or the [variableName], + /// whichever is not null. This ensures that RouteSegments with the same + /// literal or variable name will have the same hash code. + /// + /// Returns an integer hash code value. @override int get hashCode => (literal ?? variableName).hashCode; + /// Returns a string representation of the RouteSegment. + /// + /// The string representation depends on the type of the segment: + /// - For a literal matcher, it returns the literal value. + /// - For a variable segment, it returns the variable name. + /// - For a segment with a regular expression, it returns the pattern enclosed in parentheses. + /// - For a remaining matcher (wildcard), it returns "*". + /// + /// Returns a string representing the RouteSegment. @override String toString() { if (isLiteralMatcher) { @@ -75,7 +126,13 @@ class RouteSegment { } } +/// Represents a node in the route tree. class RouteNode { + /// Creates a new RouteNode from a list of route specifications. + /// + /// [specs] The list of route specifications. + /// [depth] The depth of this node in the route tree. + /// [matcher] The regular expression matcher for this node. RouteNode(List specs, {int depth = 0, RegExp? matcher}) { patternMatcher = matcher; @@ -147,22 +204,35 @@ class RouteNode { }).toList(); } + /// Creates a new RouteNode with a specific route specification. + /// + /// [specification] The route specification for this node. RouteNode.withSpecification(this.specification); - // Regular expression matcher for this node. May be null. + /// Regular expression matcher for this node. May be null. RegExp? patternMatcher; + + /// The controller associated with this route node. Controller? get controller => specification?.controller; + + /// The route specification for this node. RouteSpecification? specification; - // Includes children that are variables with and without regex patterns + /// Children nodes that are matched using regular expressions. List patternedChildren = []; - // Includes children that are literal path segments that can be matched with simple string equality + /// Children nodes that are matched using string equality. Map equalityChildren = {}; - // Valid if has child that is a take all (*) segment. + /// Child node that matches all remaining segments. RouteNode? takeAllChild; + /// Finds the appropriate node for the given path segments. + /// + /// [requestSegments] An iterator of the path segments. + /// [path] The full request path. + /// + /// Returns the matching RouteNode or null if no match is found. RouteNode? nodeForPathSegments( Iterator requestSegments, RequestPath path, @@ -195,6 +265,16 @@ class RouteNode { return takeAllChild; } + /// Generates a string representation of the RouteNode and its children. + /// + /// This method creates a hierarchical string representation of the RouteNode, + /// including information about the pattern matcher, associated controller, + /// and child nodes. The representation is indented based on the depth of the + /// node in the route tree. + /// + /// [depth] The depth of this node in the route tree, used for indentation. + /// + /// Returns a string representation of the RouteNode and its children. @override String toString({int depth = 0}) { final buf = StringBuffer(); diff --git a/packages/http/lib/src/route_specification.dart b/packages/http/lib/src/route_specification.dart index c7ee95a..dac3e38 100644 --- a/packages/http/lib/src/route_specification.dart +++ b/packages/http/lib/src/route_specification.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_http/http.dart'; /// Specifies a matchable route path. @@ -15,6 +24,11 @@ class RouteSpecification { .toList(); } + /// Creates a list of [RouteSpecification]s from a given route pattern. + /// + /// This method handles optional segments in the route pattern. + /// @param routePattern The input route pattern string. + /// @return A list of [RouteSpecification]s. static List specificationsForRoutePattern( String routePattern, ) { @@ -32,10 +46,16 @@ class RouteSpecification { /// A reference back to the [Controller] to be used when this specification is matched. Controller? controller; + /// Returns a string representation of the route specification. @override String toString() => segments.join("/"); } +/// Generates a list of path strings from a given route pattern. +/// +/// This function handles optional segments and regular expressions in the route pattern. +/// @param inputPattern The input route pattern string. +/// @return A list of path strings. List _pathsFromRoutePattern(String inputPattern) { var routePattern = inputPattern; var endingOptionalCloseCount = 0; @@ -102,6 +122,11 @@ List _pathsFromRoutePattern(String inputPattern) { return patterns; } +/// Splits a path string into a list of [RouteSegment]s. +/// +/// This function handles regular expressions within path segments. +/// @param inputPath The input path string. +/// @return A list of [RouteSegment]s. List _splitPathSegments(String inputPath) { var path = inputPath; // Once we've gotten into this method, the path has been validated for optionals and regex and optionals have been removed. diff --git a/packages/http/lib/src/router.dart b/packages/http/lib/src/router.dart index 66420d1..cd3156d 100644 --- a/packages/http/lib/src/router.dart +++ b/packages/http/lib/src/router.dart @@ -1,6 +1,14 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'dart:io'; - import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_http/http.dart'; import 'package:protevus_openapi/v3.dart'; @@ -18,6 +26,9 @@ import 'package:protevus_openapi/v3.dart'; /// a [Router] is the [ApplicationChannel.entryPoint]. class Router extends Controller { /// Creates a new [Router]. + /// + /// [basePath] is an optional prefix for all routes on this instance. + /// [notFoundHandler] is an optional function to handle requests that don't match any routes. Router({String? basePath, Future Function(Request)? notFoundHandler}) : _unmatchedController = notFoundHandler, _basePathSegments = @@ -25,9 +36,16 @@ class Router extends Controller { policy?.allowCredentials = false; } + /// The root node of the routing tree. final _RootNode _root = _RootNode(); + + /// List of route controllers. final List<_RouteController> _routeControllers = []; + + /// Segments of the base path. final List _basePathSegments; + + /// Function to handle unmatched requests. final Function(Request)? _unmatchedController; /// A prefix for all routes on this instance. @@ -77,6 +95,7 @@ class Router extends Controller { return routeController; } + /// Called when this controller is added to a channel. @override void didAddToChannel() { _root.node = @@ -95,6 +114,7 @@ class Router extends Controller { ); } + /// Routers override this method to throw an exception. Use [route] instead. @override Linkable? linkFunction( FutureOr Function(Request request) handle, @@ -104,6 +124,7 @@ class Router extends Controller { ); } + /// Receives a request and routes it to the appropriate controller. @override Future receive(Request req) async { Controller next; @@ -142,11 +163,13 @@ class Router extends Controller { return next.receive(req); } + /// Router should not handle requests directly. @override FutureOr handle(Request request) { throw StateError("Router invoked handle. This is a bug."); } + /// Documents the paths for this router. @override Map documentPaths(APIDocumentContext context) { return _routeControllers.fold({}, (prev, elem) { @@ -155,6 +178,7 @@ class Router extends Controller { }); } + /// Documents the components for this router. @override void documentComponents(APIDocumentContext context) { for (final controller in _routeControllers) { @@ -162,11 +186,13 @@ class Router extends Controller { } } + /// Returns a string representation of this router. @override String toString() { return _root.node.toString(); } + /// Handles unmatched requests. Future _handleUnhandledRequest(Request req) async { if (_unmatchedController != null) { return _unmatchedController(req); @@ -184,11 +210,14 @@ class Router extends Controller { } } +/// Represents the root node of the routing tree. class _RootNode { RouteNode? node; } +/// Represents a route controller. class _RouteController extends Controller { + /// Creates a new [_RouteController] with the given specifications. _RouteController(this.specifications) { for (final p in specifications) { p.controller = this; @@ -198,6 +227,7 @@ class _RouteController extends Controller { /// Route specifications for this controller. final List specifications; + /// Documents the paths for this route controller. @override Map documentPaths(APIDocumentContext components) { return specifications.fold({}, (pathMap, spec) { @@ -235,6 +265,7 @@ class _RouteController extends Controller { }); } + /// Handles the request for this route controller. @override FutureOr handle(Request request) { return request; diff --git a/packages/http/lib/src/serializable.dart b/packages/http/lib/src/serializable.dart index 7a7c32c..53ab213 100644 --- a/packages/http/lib/src/serializable.dart +++ b/packages/http/lib/src/serializable.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_http/http.dart'; import 'package:protevus_openapi/v3.dart'; @@ -11,6 +20,9 @@ abstract class Serializable { /// /// The returned [APISchemaObject] will be of type [APIType.object]. By default, each instance variable /// of the receiver's type will be a property of the return value. + /// + /// [context] The API document context. + /// Returns an [APISchemaObject] representing the schema of this serializable object. APISchemaObject documentSchema(APIDocumentContext context) { return (RuntimeContext.current[runtimeType] as SerializableRuntime) .documentSchema(context); @@ -24,6 +36,8 @@ abstract class Serializable { /// This method is used by implementors to assign and use values from [object] for its own /// purposes. [SerializableException]s should be thrown when [object] violates a constraint /// of the receiver. + /// + /// [object] The map containing the values to be read. void readFromMap(Map object); /// Reads values from [object], after applying filters. @@ -31,17 +45,11 @@ abstract class Serializable { /// The key name must exactly match the name of the property as defined in the receiver's type. /// If [object] contains a key that is unknown to the receiver, an exception is thrown (status code: 400). /// - /// [accept], [ignore], [reject] and [require] are filters on [object]'s keys with the following behaviors: - /// - /// If [accept] is set, all values for the keys that are not given are ignored and discarded. - /// If [ignore] is set, all values for the given keys are ignored and discarded. - /// If [reject] is set, if [object] contains any of these keys, a status code 400 exception is thrown. - /// If [require] is set, all keys must be present in [object]. - /// - /// Usage: - /// var values = json.decode(await request.body.decode()); - /// var user = User() - /// ..read(values, ignore: ["id"]); + /// [object] The map containing the values to be read. + /// [accept] If set, only these keys will be accepted from the object. + /// [ignore] If set, these keys will be ignored from the object. + /// [reject] If set, the presence of any of these keys will cause an exception. + /// [require] If set, all of these keys must be present in the object. void read( Map object, { Iterable? accept, @@ -82,6 +90,8 @@ abstract class Serializable { /// If a [Response.body]'s type implements this interface, this method is invoked prior to any content-type encoding /// performed by the [Response]. A [Response.body] may also be a [List], for which this method is invoked on /// each element in the list. + /// + /// Returns a [Map] representation of the object. Map asMap(); /// Whether a subclass will automatically be registered as a schema component automatically. @@ -94,11 +104,19 @@ abstract class Serializable { static bool get shouldAutomaticallyDocument => true; } +/// Exception thrown when there's an error in serialization or deserialization. class SerializableException implements HandlerException { + /// Constructor for SerializableException. + /// + /// [reasons] A list of reasons for the exception. SerializableException(this.reasons); + /// The reasons for the exception. final List reasons; + /// Generates a response for this exception. + /// + /// Returns a [Response] with a bad request status and error details. @override Response get response { return Response.badRequest( @@ -106,6 +124,9 @@ class SerializableException implements HandlerException { ); } + /// Returns a string representation of the exception. + /// + /// Returns a string containing the error and reasons. @override String toString() { final errorString = response.body["error"] as String?; @@ -114,6 +135,11 @@ class SerializableException implements HandlerException { } } +/// Abstract class representing the runtime behavior of a Serializable object. abstract class SerializableRuntime { + /// Documents the schema of a Serializable object. + /// + /// [context] The API document context. + /// Returns an [APISchemaObject] representing the schema of the Serializable object. APISchemaObject documentSchema(APIDocumentContext context); }