update: updating files with detailed comments
This commit is contained in:
parent
ef243a6e8b
commit
0a3d903320
22 changed files with 1005 additions and 55 deletions
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue