update: updating files with detailed comments

This commit is contained in:
Patrick Stewart 2024-09-08 23:08:53 -07:00
parent ef243a6e8b
commit 0a3d903320
22 changed files with 1005 additions and 55 deletions

View file

@ -1,3 +1,26 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -1,14 +1,26 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<List<int>> bodyByteStream)
: _originalByteStream = bodyByteStream;
@ -67,8 +79,13 @@ abstract class BodyDecoder {
return _bytes;
}
/// The original byte stream to be decoded.
final Stream<List<int>> _originalByteStream;
/// The decoded data after processing.
dynamic _decodedData;
/// The original bytes, if retained.
List<int>? _bytes;
/// Decodes this object's bytes as [T].
@ -127,6 +144,9 @@ abstract class BodyDecoder {
return _cast<T>(_decodedData);
}
/// Casts the decoded body to the specified type [T].
///
/// Throws a [Response.badRequest] if the casting fails.
T _cast<T>(dynamic body) {
try {
return RuntimeContext.current.coerce<T>(body);
@ -137,6 +157,7 @@ abstract class BodyDecoder {
}
}
/// Reads all bytes from the given [stream] and returns them as a [List<int>].
Future<List<int>> _readBytes(Stream<List<int>> stream) async {
return (await stream.toList()).expand((e) => e).toList();
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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";

View file

@ -1,6 +1,14 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, APIPath> 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<String, APIOperation> 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<T> extends Controller {
_ControllerRecycler(this.generator, Recyclable<T> instance) {
@ -356,14 +415,21 @@ class _ControllerRecycler<T> 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<T>? _nextInstanceToReceive;
/// The next instance to receive requests.
Recyclable<T>? get nextInstanceToReceive => _nextInstanceToReceive;
/// Sets the next instance to receive requests and initializes it.
set nextInstanceToReceive(Recyclable<T>? instance) {
_nextInstanceToReceive = instance;
instance?.restore(recycleState);
@ -373,16 +439,41 @@ class _ControllerRecycler<T> 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<T> 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<RequestOrResponse?>].
///
/// Returns:
/// The newly linked [Linkable] controller, or null if the linking failed.
@override
Linkable? linkFunction(
FutureOr<RequestOrResponse?> Function(Request request) handle,
@ -399,6 +500,22 @@ class _ControllerRecycler<T> 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<T> 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<RequestOrResponse> 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<T> 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<String, APIPath> 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<String, APIOperation> documentOperations(
APIDocumentContext components,
@ -435,17 +612,49 @@ class _ControllerRecycler<T> 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<RequestOrResponse?> 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<RequestOrResponse?> 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<String, APIOperation> 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;
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String> 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<String, dynamic> 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"),

View file

@ -1,11 +1,20 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<Response> 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<String, ContentType> _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<String, ContentType> _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<Response> Function(
FileController,
Request,
@ -155,6 +172,7 @@ class FileController extends Controller {
?.policy;
}
/// Handles incoming requests and serves the appropriate file.
@override
Future<RequestOrResponse> 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<String, APIOperation> 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;
}

View file

@ -1,9 +1,31 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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;
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, Codec> _primaryTypeCodecs = {};
/// Map of fully specified content types to their respective codecs.
final Map<String, Map<String, Codec>> _fullySpecificedCodecs = {};
/// Map of primary content types to their compression settings.
final Map<String, bool> _primaryTypeCompressionMap = {};
/// Map of fully specified content types to their compression settings.
final Map<String, Map<String, bool>> _fullySpecifiedCompressionMap = {};
/// Map of content types to their default charsets.
final Map<String, Map<String, String?>> _defaultCharsetMap = {};
/// Adds a custom [codec] for [contentType].
@ -167,6 +186,7 @@ class CodecRegistry {
return null;
}
/// Returns a [Codec] for the given [charset].
Codec<String, List<int>> _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<String, List<int>>? _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<Map<String, dynamic>?, dynamic> {
const _FormCodec();
@ -201,6 +223,7 @@ class _FormCodec extends Codec<Map<String, dynamic>?, dynamic> {
Converter<String, Map<String, dynamic>> get decoder => const _FormDecoder();
}
/// A [Converter] for encoding form data.
class _FormEncoder extends Converter<Map<String, dynamic>, String> {
const _FormEncoder();
@ -209,6 +232,7 @@ class _FormEncoder extends Converter<Map<String, dynamic>, 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<String>) {
@ -223,6 +247,7 @@ class _FormEncoder extends Converter<Map<String, dynamic>, String> {
}
}
/// A [Converter] for decoding form data.
class _FormDecoder extends Converter<String, Map<String, dynamic>> {
// This class may take input as either String or List<int>. If charset is not defined in request,
// then data is List<int> (from CodecRegistry) and will default to being UTF8 decoded first.
@ -230,29 +255,81 @@ class _FormDecoder extends Converter<String, Map<String, dynamic>> {
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<String, dynamic>] where the keys are the form field names
/// and the values are either a single String or a List<String> 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<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> outSink) {
return _FormSink(outSink);
}
}
/// A [ChunkedConversionSink] for form data.
class _FormSink implements ChunkedConversionSink<String> {
_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<Map<String, dynamic>> _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<String, dynamic>.
/// 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()));

View file

@ -1,5 +1,13 @@
import 'dart:async';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<InstanceType extends ManagedObject>
extends ResourceController {
/// Creates an instance of a [ManagedObjectController].
///
/// [context] is the [ManagedContext] used for database operations.
ManagedObjectController(ManagedContext context) : super() {
_query = Query<InstanceType>(context);
}
@ -49,6 +59,8 @@ class ManagedObjectController<InstanceType extends ManagedObject>
/// 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<InstanceType extends ManagedObject>
/// 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<InstanceType>? _query;
/// Executed prior to a fetch by ID query.
@ -92,6 +107,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
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<Response> getObject(@Bind.path("id") String id) async {
final primaryKey = _query!.entity.primaryKey;
@ -128,6 +146,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.ok(object);
}
/// Handles POST requests to create a new object.
@Operation.post()
Future<Response> createObject() async {
final instance = _query!.entity.instanceOf() as InstanceType;
@ -165,6 +184,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
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<Response> deleteObject(@Bind.path("id") String id) async {
final primaryKey = _query!.entity.primaryKey;
@ -208,6 +230,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
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<Response> updateObject(@Bind.path("id") String id) async {
final primaryKey = _query!.entity.primaryKey;
@ -247,6 +272,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.ok(objects);
}
/// Handles GET requests to fetch multiple objects.
///
/// Supports pagination, sorting, and filtering through query parameters.
@Operation.get()
Future<Response> getObjects({
/// Limits the number of objects returned.
@ -359,6 +387,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return didFindObjects(results);
}
/// Documents the request body for POST and PUT operations.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
@ -375,6 +404,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return null;
}
/// Documents the responses for each operation type.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -445,6 +475,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return {};
}
/// Documents the operations for this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -469,6 +500,10 @@ class ManagedObjectController<InstanceType extends ManagedObject>
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<InstanceType extends ManagedObject>
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, {

View file

@ -1,5 +1,13 @@
import 'dart:async';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<InstanceType extends ManagedObject>
extends ResourceController {
/// Create an instance of [QueryController].
///
/// [context] is the [ManagedContext] used for database operations.
QueryController(ManagedContext context) : super() {
query = Query<InstanceType>(context);
}
@ -34,6 +44,12 @@ abstract class QueryController<InstanceType extends ManagedObject>
/// 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<InstanceType>? 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<RequestOrResponse> willProcessRequest(Request req) {
if (req.path.orderedVariableNames.isNotEmpty) {
@ -64,6 +80,12 @@ abstract class QueryController<InstanceType extends ManagedObject>
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());

View file

@ -1,7 +1,15 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<void Function(Response)>? _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<ContentType>? _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<int>? _responseBodyBytes(
Response resp,
_Reference<String> 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<List<int>> _responseBodyStream(
Response resp,
_Reference<String> 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<T> {
/// Creates a new [_Reference] with the given initial value.
_Reference(this.value);
/// The wrapped value.
T? value;
}

View file

@ -1,6 +1,14 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<List<int>> 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<List<int>> _originalByteStream;
/// A buffering controller for the byte stream when content length is not specified.
StreamController<List<int>>? _bufferingController;
/// The number of bytes read from the request body.
int _bytesRead = 0;
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, String> variables = {};
/// A list of the segments in a matched path.

View file

@ -1,7 +1,15 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<void> {
/// 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<RequestOrResponse> 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<String, APIOperation> 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<String> _allowedMethodsForPathVariables(
Iterable<String?> pathVariables,
) {
@ -258,6 +275,7 @@ abstract class ResourceController extends Controller
.toList();
}
/// Processes the request and returns a response.
Future<Response> _process() async {
if (!request!.body.isEmpty) {
if (!_requestContentTypeIsSupported(request)) {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String>? accept;
/// List of keys to ignore in the request body (for body bindings).
final List<String>? ignore;
/// List of keys to reject in the request body (for body bindings).
final List<String>? reject;
/// List of keys required in the request body (for body bindings).
final List<String>? 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();
}

View file

@ -1,17 +1,36 @@
import 'dart:async';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<ResourceControllerParameter>? ivarParameters;
/// List of operations supported by the ResourceController.
late List<ResourceControllerOperation> 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<String?> 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<APIParameter> 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<String, APIOperation> 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<AuthScope>? scopes;
/// The path variables for this operation.
final List<String> 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<ResourceControllerParameter> positionalParameters;
/// The named parameters for this operation.
final List<ResourceControllerParameter> namedParameters;
/// The function to invoke this operation.
final Future<Response> 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<String?> 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<T>({
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<String>? acceptFilter;
/// The filter for ignored values.
final List<String>? ignoreFilter;
/// The filter for required values.
final List<String>? requireFilter;
/// The filter for rejected values.
final List<String>? 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<String, dynamic> instanceVariables;
/// The named arguments for the invocation.
late Map<String, dynamic> namedArguments;
/// The positional arguments for the invocation.
late List<dynamic> positionalArguments;
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String> scopes;
}

View file

@ -1,8 +1,16 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, dynamic>? headers, dynamic body) {
this.body = body;
this.headers = LinkedHashMap<String, dynamic>(
@ -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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic> get headers => _headers;
/// Sets the headers for this response.
///
/// Clears existing headers and adds all headers from the provided map.
set headers(Map<String, dynamic> 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<String, dynamic> _headers = LinkedHashMap<String, Object?>(
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<String, dynamic> _headersWith(
Map<String, dynamic>? inputHeaders,
Map<String, dynamic> otherHeaders,

View file

@ -1,6 +1,19 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<RouteSpecification?> 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<RouteNode> 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<String, RouteNode> 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<String> 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();

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<RouteSpecification> 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<String> _pathsFromRoutePattern(String inputPattern) {
var routePattern = inputPattern;
var endingOptionalCloseCount = 0;
@ -102,6 +122,11 @@ List<String> _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<RouteSegment> _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.

View file

@ -1,6 +1,14 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String> _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<RequestOrResponse?> 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<RequestOrResponse> handle(Request request) {
throw StateError("Router invoked handle. This is a bug.");
}
/// Documents the paths for this router.
@override
Map<String, APIPath> documentPaths(APIDocumentContext context) {
return _routeControllers.fold(<String, APIPath>{}, (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<RouteSpecification> specifications;
/// Documents the paths for this route controller.
@override
Map<String, APIPath> documentPaths(APIDocumentContext components) {
return specifications.fold(<String, APIPath>{}, (pathMap, spec) {
@ -235,6 +265,7 @@ class _RouteController extends Controller {
});
}
/// Handles the request for this route controller.
@override
FutureOr<RequestOrResponse> handle(Request request) {
return request;

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, dynamic> 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<String, dynamic> object, {
Iterable<String>? 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<Serializable>], for which this method is invoked on
/// each element in the list.
///
/// Returns a [Map<String, dynamic>] representation of the object.
Map<String, dynamic> 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<String> 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);
}