Compare commits
3 commits
ef243a6e8b
...
ea26e024ce
Author | SHA1 | Date | |
---|---|---|---|
|
ea26e024ce | ||
|
5fd57e1ebd | ||
|
0a3d903320 |
38 changed files with 1575 additions and 104 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);
|
||||
}
|
||||
|
|
|
@ -7,15 +7,6 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 provides functionality for working with isolates in Dart.
|
||||
/// It exports three main components:
|
||||
/// 1. Executable: Defines the structure for tasks that can be executed in isolates.
|
||||
|
|
|
@ -11,25 +11,48 @@ import 'dart:async';
|
|||
import 'dart:isolate';
|
||||
import 'dart:mirrors';
|
||||
|
||||
/// An abstract class representing an executable task in an isolate.
|
||||
///
|
||||
/// This class provides a framework for executing tasks in separate isolates,
|
||||
/// with built-in communication capabilities.
|
||||
abstract class Executable<T extends Object?> {
|
||||
/// Constructor for the Executable class.
|
||||
///
|
||||
/// @param message A map containing the message data, including a SendPort.
|
||||
Executable(this.message) : _sendPort = message["_sendPort"];
|
||||
|
||||
/// Abstract method to be implemented by subclasses.
|
||||
///
|
||||
/// This method should contain the main logic of the task to be executed.
|
||||
/// @returns A Future that completes with the result of type T.
|
||||
Future<T> execute();
|
||||
|
||||
/// The message data passed to the Executable.
|
||||
final Map<String, dynamic> message;
|
||||
|
||||
/// A SendPort for communicating back to the main isolate.
|
||||
final SendPort? _sendPort;
|
||||
|
||||
/// Creates an instance of a specified type using reflection.
|
||||
///
|
||||
/// @param typeName The name of the type to instantiate.
|
||||
/// @param positionalArguments List of positional arguments for the constructor.
|
||||
/// @param namedArguments Map of named arguments for the constructor.
|
||||
/// @param constructorName The name of the constructor to use.
|
||||
/// @returns An instance of the specified type U.
|
||||
U instanceOf<U>(
|
||||
String typeName, {
|
||||
List positionalArguments = const [],
|
||||
Map<Symbol, dynamic> namedArguments = const {},
|
||||
Symbol constructorName = Symbol.empty,
|
||||
}) {
|
||||
// Try to find the ClassMirror in the root library
|
||||
ClassMirror? typeMirror = currentMirrorSystem()
|
||||
.isolate
|
||||
.rootLibrary
|
||||
.declarations[Symbol(typeName)] as ClassMirror?;
|
||||
|
||||
// If not found in the root library, search in all libraries
|
||||
typeMirror ??= currentMirrorSystem()
|
||||
.libraries
|
||||
.values
|
||||
|
@ -44,6 +67,7 @@ abstract class Executable<T extends Object?> {
|
|||
),
|
||||
) as ClassMirror?;
|
||||
|
||||
// Create and return a new instance of the specified type
|
||||
return typeMirror!
|
||||
.newInstance(
|
||||
constructorName,
|
||||
|
@ -53,10 +77,16 @@ abstract class Executable<T extends Object?> {
|
|||
.reflectee as U;
|
||||
}
|
||||
|
||||
/// Sends a message back to the main isolate.
|
||||
///
|
||||
/// @param message The message to be sent.
|
||||
void send(dynamic message) {
|
||||
_sendPort!.send(message);
|
||||
}
|
||||
|
||||
/// Logs a message by sending it back to the main isolate.
|
||||
///
|
||||
/// @param message The message to be logged.
|
||||
void log(String message) {
|
||||
_sendPort!.send({"_line_": message});
|
||||
}
|
||||
|
|
|
@ -12,25 +12,64 @@ import 'dart:io';
|
|||
import 'dart:isolate';
|
||||
import 'package:protevus_isolate/isolate.dart';
|
||||
|
||||
/// A class that manages the execution of code in an isolate.
|
||||
///
|
||||
/// This class provides functionality to run code in a separate isolate,
|
||||
/// allowing for concurrent execution and isolation of resources.
|
||||
/// It handles the creation of the isolate, communication between the
|
||||
/// main isolate and the spawned isolate, and manages the lifecycle
|
||||
/// of the execution.
|
||||
class IsolateExecutor<U> {
|
||||
/// Creates an instance of IsolateExecutor.
|
||||
///
|
||||
/// [generator] is the [SourceGenerator] that provides the source code
|
||||
/// to be executed in the isolate.
|
||||
/// [packageConfigURI] is the optional URI of the package configuration file.
|
||||
/// If provided, it will be used for package resolution in the isolate.
|
||||
/// [message] is an optional map of data to be passed to the isolate.
|
||||
/// This data will be available to the code running in the isolate.
|
||||
IsolateExecutor(
|
||||
this.generator, {
|
||||
this.packageConfigURI,
|
||||
this.message = const {},
|
||||
});
|
||||
|
||||
/// The source generator that provides the code to be executed.
|
||||
final SourceGenerator generator;
|
||||
|
||||
/// A map of data to be passed to the isolate.
|
||||
final Map<String, dynamic> message;
|
||||
|
||||
/// The URI of the package configuration file.
|
||||
final Uri? packageConfigURI;
|
||||
|
||||
/// A completer that completes when the isolate execution is finished.
|
||||
final Completer completer = Completer();
|
||||
|
||||
/// Stream of events from the isolate.
|
||||
///
|
||||
/// This stream emits any custom events sent from the isolate during execution.
|
||||
Stream<dynamic> get events => _eventListener.stream;
|
||||
|
||||
/// Stream of console output from the isolate.
|
||||
///
|
||||
/// This stream emits any console output (print statements, etc.) from the isolate.
|
||||
Stream<String> get console => _logListener.stream;
|
||||
|
||||
/// StreamController for managing console output from the isolate.
|
||||
final StreamController<String> _logListener = StreamController<String>();
|
||||
|
||||
/// StreamController for managing custom events from the isolate.
|
||||
final StreamController<dynamic> _eventListener = StreamController<dynamic>();
|
||||
|
||||
/// Executes the code in the isolate and returns the result.
|
||||
///
|
||||
/// This method spawns a new isolate, runs the provided code, and returns
|
||||
/// the result. It handles error cases and ensures proper cleanup of resources.
|
||||
///
|
||||
/// Throws a [StateError] if the package configuration file is not found.
|
||||
///
|
||||
/// Returns a [Future] that completes with the result of the isolate execution.
|
||||
Future<U> execute() async {
|
||||
if (packageConfigURI != null &&
|
||||
!File.fromUri(packageConfigURI!).existsSync()) {
|
||||
|
@ -93,6 +132,21 @@ class IsolateExecutor<U> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Runs an executable in an isolate.
|
||||
///
|
||||
/// This static method provides a convenient way to execute code in an isolate.
|
||||
/// It creates a [SourceGenerator], sets up an [IsolateExecutor], and manages
|
||||
/// the execution process.
|
||||
///
|
||||
/// [executable] is an instance of [Executable<T>] containing the code to be executed.
|
||||
/// [imports] is an optional list of import statements to be included in the isolate.
|
||||
/// [packageConfigURI] is the optional URI of the package configuration file.
|
||||
/// [additionalContents] is optional additional code to be included in the isolate.
|
||||
/// [additionalTypes] is an optional list of additional types to be included in the isolate.
|
||||
/// [eventHandler] is an optional function to handle events from the isolate.
|
||||
/// [logHandler] is an optional function to handle console output from the isolate.
|
||||
///
|
||||
/// Returns a [Future] that completes with the result of type [T] from the isolate execution.
|
||||
static Future<T> run<T>(
|
||||
Executable<T> executable, {
|
||||
List<String> imports = const [],
|
||||
|
|
|
@ -20,7 +20,14 @@ import 'package:analyzer/file_system/physical_file_system.dart';
|
|||
import 'package:path/path.dart';
|
||||
import 'package:protevus_isolate/isolate.dart';
|
||||
|
||||
/// A class responsible for generating source code for isolate execution.
|
||||
class SourceGenerator {
|
||||
/// Constructs a SourceGenerator instance.
|
||||
///
|
||||
/// [executableType]: The Type of the executable class.
|
||||
/// [imports]: List of import statements to include in the generated source.
|
||||
/// [additionalTypes]: List of additional Types to include in the generated source.
|
||||
/// [additionalContents]: Optional additional content to append to the generated source.
|
||||
SourceGenerator(
|
||||
this.executableType, {
|
||||
this.imports = const [],
|
||||
|
@ -28,24 +35,40 @@ class SourceGenerator {
|
|||
this.additionalContents,
|
||||
});
|
||||
|
||||
/// The Type of the executable class.
|
||||
Type executableType;
|
||||
|
||||
/// Returns the name of the executable type.
|
||||
String get typeName =>
|
||||
MirrorSystem.getName(reflectType(executableType).simpleName);
|
||||
|
||||
/// List of import statements to include in the generated source.
|
||||
final List<String> imports;
|
||||
|
||||
/// Optional additional content to append to the generated source.
|
||||
final String? additionalContents;
|
||||
|
||||
/// List of additional Types to include in the generated source.
|
||||
final List<Type> additionalTypes;
|
||||
|
||||
/// Generates the complete script source for isolate execution.
|
||||
///
|
||||
/// Returns a Future<String> containing the generated source code.
|
||||
Future<String> get scriptSource async {
|
||||
final typeSource = (await _getClass(executableType)).toSource();
|
||||
final builder = StringBuffer();
|
||||
|
||||
// Add standard imports
|
||||
builder.writeln("import 'dart:async';");
|
||||
builder.writeln("import 'dart:isolate';");
|
||||
builder.writeln("import 'dart:mirrors';");
|
||||
|
||||
// Add custom imports
|
||||
for (final anImport in imports) {
|
||||
builder.writeln("import '$anImport';");
|
||||
}
|
||||
|
||||
// Add main function for isolate execution
|
||||
builder.writeln(
|
||||
"""
|
||||
Future main (List<String> args, Map<String, dynamic> message) async {
|
||||
|
@ -56,14 +79,20 @@ Future main (List<String> args, Map<String, dynamic> message) async {
|
|||
}
|
||||
""",
|
||||
);
|
||||
|
||||
// Add executable class source
|
||||
builder.writeln(typeSource);
|
||||
|
||||
// Add Executable base class source
|
||||
builder.writeln((await _getClass(Executable)).toSource());
|
||||
|
||||
// Add additional types' sources
|
||||
for (final type in additionalTypes) {
|
||||
final source = await _getClass(type);
|
||||
builder.writeln(source.toSource());
|
||||
}
|
||||
|
||||
// Add additional contents if provided
|
||||
if (additionalContents != null) {
|
||||
builder.writeln(additionalContents);
|
||||
}
|
||||
|
@ -71,6 +100,10 @@ Future main (List<String> args, Map<String, dynamic> message) async {
|
|||
return builder.toString();
|
||||
}
|
||||
|
||||
/// Retrieves the ClassDeclaration for a given Type.
|
||||
///
|
||||
/// [type]: The Type to retrieve the ClassDeclaration for.
|
||||
/// Returns a Future<ClassDeclaration>.
|
||||
static Future<ClassDeclaration> _getClass(Type type) async {
|
||||
final uri =
|
||||
await Isolate.resolvePackageUri(reflectClass(type).location!.sourceUri);
|
||||
|
@ -88,6 +121,11 @@ Future main (List<String> args, Map<String, dynamic> message) async {
|
|||
}
|
||||
}
|
||||
|
||||
/// Creates an AnalysisContext for a given file path.
|
||||
///
|
||||
/// [path]: The file path to create the context for.
|
||||
/// [resourceProvider]: Optional ResourceProvider, defaults to PhysicalResourceProvider.INSTANCE.
|
||||
/// Returns an AnalysisContext.
|
||||
AnalysisContext _createContext(
|
||||
String path, {
|
||||
ResourceProvider? resourceProvider,
|
||||
|
|
|
@ -7,10 +7,15 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/// The main library for the Protevus runtime package.
|
||||
///
|
||||
/// This library provides core functionality for the Protevus Platform,
|
||||
/// including compilation, analysis, and runtime management.
|
||||
library runtime;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
// Export statements for various components of the runtime package
|
||||
export 'package:protevus_runtime/src/analyzer.dart';
|
||||
export 'package:protevus_runtime/src/build.dart';
|
||||
export 'package:protevus_runtime/src/build_context.dart';
|
||||
|
@ -21,19 +26,36 @@ export 'package:protevus_runtime/src/exceptions.dart';
|
|||
export 'package:protevus_runtime/src/generator.dart';
|
||||
export 'package:protevus_runtime/src/mirror_coerce.dart';
|
||||
export 'package:protevus_runtime/src/mirror_context.dart';
|
||||
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
|
||||
/// Compiler for the runtime package itself.
|
||||
/// A specialized compiler for the runtime package itself.
|
||||
///
|
||||
/// Removes dart:mirror from a replica of this package, and adds
|
||||
/// a generated runtime to the replica's pubspec.
|
||||
/// This compiler is responsible for creating a mirror-free version of the
|
||||
/// runtime package. It removes dart:mirror dependencies and adds a generated
|
||||
/// runtime to the package's pubspec.
|
||||
class RuntimePackageCompiler extends Compiler {
|
||||
/// Compiles the runtime package.
|
||||
///
|
||||
/// This method is currently a no-op, returning an empty map. It may be
|
||||
/// extended in the future to perform actual compilation tasks.
|
||||
///
|
||||
/// [context] - The mirror context for compilation (unused in this implementation).
|
||||
///
|
||||
/// Returns an empty map, as no compilation is currently performed.
|
||||
@override
|
||||
Map<String, Object> compile(MirrorContext context) => {};
|
||||
|
||||
/// Modifies the package structure to remove mirror dependencies.
|
||||
///
|
||||
/// This method performs the following tasks:
|
||||
/// 1. Rewrites the main library file to remove mirror-related exports.
|
||||
/// 2. Updates the context file to use the generated runtime instead of mirror context.
|
||||
/// 3. Modifies the pubspec.yaml to include the generated runtime as a dependency.
|
||||
///
|
||||
/// [destinationDirectory] - The directory where the modified package will be created.
|
||||
@override
|
||||
void deflectPackage(Directory destinationDirectory) {
|
||||
// Rewrite the main library file
|
||||
final libraryFile = File.fromUri(
|
||||
destinationDirectory.uri.resolve("lib/").resolve("runtime.dart"),
|
||||
);
|
||||
|
@ -41,6 +63,7 @@ class RuntimePackageCompiler extends Compiler {
|
|||
"library runtime;\nexport 'src/context.dart';\nexport 'src/exceptions.dart';",
|
||||
);
|
||||
|
||||
// Update the context file
|
||||
final contextFile = File.fromUri(
|
||||
destinationDirectory.uri
|
||||
.resolve("lib/")
|
||||
|
@ -53,6 +76,7 @@ class RuntimePackageCompiler extends Compiler {
|
|||
);
|
||||
contextFile.writeAsStringSync(contextFileContents);
|
||||
|
||||
// Modify the pubspec.yaml
|
||||
final pubspecFile =
|
||||
File.fromUri(destinationDirectory.uri.resolve("pubspec.yaml"));
|
||||
final pubspecContents = pubspecFile.readAsStringSync().replaceFirst(
|
||||
|
|
|
@ -9,12 +9,30 @@
|
|||
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
|
||||
/// Prefix string for List types in type checking.
|
||||
const String _listPrefix = "List<";
|
||||
|
||||
/// Prefix string for Map types in type checking.
|
||||
const String _mapPrefix = "Map<String,";
|
||||
|
||||
/// Casts a dynamic input to a specified type T.
|
||||
///
|
||||
/// This function attempts to cast the input to the specified type T.
|
||||
/// It handles nullable types, Lists, and Maps with various element types.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - input: The dynamic value to be cast.
|
||||
///
|
||||
/// Returns:
|
||||
/// The input cast to type T.
|
||||
///
|
||||
/// Throws:
|
||||
/// - [TypeCoercionException] if the casting fails.
|
||||
T cast<T>(dynamic input) {
|
||||
try {
|
||||
var typeString = T.toString();
|
||||
|
||||
// Handle nullable types
|
||||
if (typeString.endsWith('?')) {
|
||||
if (input == null) {
|
||||
return null as T;
|
||||
|
@ -22,11 +40,14 @@ T cast<T>(dynamic input) {
|
|||
typeString = typeString.substring(0, typeString.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle List types
|
||||
if (typeString.startsWith(_listPrefix)) {
|
||||
if (input is! List) {
|
||||
throw TypeError();
|
||||
}
|
||||
|
||||
// Cast List to various element types
|
||||
if (typeString.startsWith("List<int>")) {
|
||||
return List<int>.from(input) as T;
|
||||
} else if (typeString.startsWith("List<num>")) {
|
||||
|
@ -50,12 +71,16 @@ T cast<T>(dynamic input) {
|
|||
} else if (typeString.startsWith("List<Map<String, dynamic>>")) {
|
||||
return List<Map<String, dynamic>>.from(input) as T;
|
||||
}
|
||||
} else if (typeString.startsWith(_mapPrefix)) {
|
||||
}
|
||||
// Handle Map types
|
||||
else if (typeString.startsWith(_mapPrefix)) {
|
||||
if (input is! Map) {
|
||||
throw TypeError();
|
||||
}
|
||||
|
||||
final inputMap = input as Map<String, dynamic>;
|
||||
|
||||
// Cast Map to various value types
|
||||
if (typeString.startsWith("Map<String, int>")) {
|
||||
return Map<String, int>.from(inputMap) as T;
|
||||
} else if (typeString.startsWith("Map<String, num>")) {
|
||||
|
@ -79,8 +104,10 @@ T cast<T>(dynamic input) {
|
|||
}
|
||||
}
|
||||
|
||||
// If no specific casting is needed, return the input as T
|
||||
return input as T;
|
||||
} on TypeError {
|
||||
// If a TypeError occurs during casting, throw a TypeCoercionException
|
||||
throw TypeCoercionException(T, input.runtimeType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,11 @@ import 'package:analyzer/dart/ast/ast.dart';
|
|||
import 'package:analyzer/file_system/physical_file_system.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
/// A class for analyzing Dart code.
|
||||
class CodeAnalyzer {
|
||||
/// Constructs a CodeAnalyzer with the given URI.
|
||||
///
|
||||
/// Throws an [ArgumentError] if the URI is not absolute or if no analysis context is found.
|
||||
CodeAnalyzer(this.uri) {
|
||||
if (!uri.isAbsolute) {
|
||||
throw ArgumentError("'uri' must be absolute for CodeAnalyzer");
|
||||
|
@ -27,16 +31,23 @@ class CodeAnalyzer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the path from the URI.
|
||||
String get path {
|
||||
return getPath(uri);
|
||||
}
|
||||
|
||||
/// The URI of the code to analyze.
|
||||
late final Uri uri;
|
||||
|
||||
/// The collection of analysis contexts.
|
||||
late AnalysisContextCollection contexts;
|
||||
|
||||
/// A cache of resolved ASTs.
|
||||
final _resolvedAsts = <String, AnalysisResult>{};
|
||||
|
||||
/// Resolves the unit or library at the given URI.
|
||||
///
|
||||
/// Returns a [Future] that completes with an [AnalysisResult].
|
||||
Future<AnalysisResult> resolveUnitOrLibraryAt(Uri uri) async {
|
||||
if (FileSystemEntity.isFileSync(
|
||||
uri.toFilePath(windows: Platform.isWindows),
|
||||
|
@ -47,6 +58,10 @@ class CodeAnalyzer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolves the library at the given URI.
|
||||
///
|
||||
/// Returns a [Future] that completes with a [ResolvedLibraryResult].
|
||||
/// Throws an [ArgumentError] if the URI could not be resolved.
|
||||
Future<ResolvedLibraryResult> resolveLibraryAt(Uri uri) async {
|
||||
assert(
|
||||
FileSystemEntity.isDirectorySync(
|
||||
|
@ -68,6 +83,10 @@ class CodeAnalyzer {
|
|||
"${contexts.contexts.map((c) => c.contextRoot.root.toUri()).join(", ")})");
|
||||
}
|
||||
|
||||
/// Resolves the unit at the given URI.
|
||||
///
|
||||
/// Returns a [Future] that completes with a [ResolvedUnitResult].
|
||||
/// Throws an [ArgumentError] if the URI could not be resolved.
|
||||
Future<ResolvedUnitResult> resolveUnitAt(Uri uri) async {
|
||||
assert(
|
||||
FileSystemEntity.isFileSync(
|
||||
|
@ -89,6 +108,9 @@ class CodeAnalyzer {
|
|||
"${contexts.contexts.map((c) => c.contextRoot.root.toUri()).join(", ")})");
|
||||
}
|
||||
|
||||
/// Gets the class declaration from the file at the given URI.
|
||||
///
|
||||
/// Returns null if the class is not found or if there's an error.
|
||||
ClassDeclaration? getClassFromFile(String className, Uri fileUri) {
|
||||
try {
|
||||
return _getFileAstRoot(fileUri)
|
||||
|
@ -103,6 +125,9 @@ class CodeAnalyzer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets all subclasses of the given superclass from the file at the given URI.
|
||||
///
|
||||
/// Returns a list of [ClassDeclaration]s.
|
||||
List<ClassDeclaration> getSubclassesFromFile(
|
||||
String superclassName,
|
||||
Uri fileUri,
|
||||
|
@ -115,6 +140,9 @@ class CodeAnalyzer {
|
|||
.toList();
|
||||
}
|
||||
|
||||
/// Gets the AST root of the file at the given URI.
|
||||
///
|
||||
/// Returns a [CompilationUnit].
|
||||
CompilationUnit _getFileAstRoot(Uri fileUri) {
|
||||
assert(
|
||||
FileSystemEntity.isFileSync(
|
||||
|
@ -135,6 +163,9 @@ class CodeAnalyzer {
|
|||
return unit.unit;
|
||||
}
|
||||
|
||||
/// Converts the input URI to a normalized path string.
|
||||
///
|
||||
/// This is a static utility method.
|
||||
static String getPath(dynamic inputUri) {
|
||||
return PhysicalResourceProvider.INSTANCE.pathContext.normalize(
|
||||
PhysicalResourceProvider.INSTANCE.pathContext.fromUri(inputUri),
|
||||
|
|
|
@ -11,16 +11,34 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:mirrors';
|
||||
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
import 'package:io/io.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
|
||||
/// A class responsible for building and compiling the application.
|
||||
///
|
||||
/// This class handles the entire build process, including resolving ASTs,
|
||||
/// generating runtime, compiling packages, and creating the final executable.
|
||||
class Build {
|
||||
/// Creates a new [Build] instance with the given [BuildContext].
|
||||
///
|
||||
/// [context] is the build context containing necessary information for the build process.
|
||||
Build(this.context);
|
||||
|
||||
/// The build context for this build operation.
|
||||
final BuildContext context;
|
||||
|
||||
/// Executes the build process.
|
||||
///
|
||||
/// This method performs the following steps:
|
||||
/// 1. Resolves ASTs
|
||||
/// 2. Generates runtime
|
||||
/// 3. Compiles packages
|
||||
/// 4. Prepares the build directory
|
||||
/// 5. Fetches dependencies
|
||||
/// 6. Compiles the final executable (if not for tests)
|
||||
///
|
||||
/// Throws a [StateError] if any step fails.
|
||||
Future execute() async {
|
||||
final compilers = context.context.compilers;
|
||||
|
||||
|
@ -129,6 +147,10 @@ class Build {
|
|||
}
|
||||
}
|
||||
|
||||
/// Fetches dependencies for the project.
|
||||
///
|
||||
/// This method runs 'dart pub get' with the '--offline' and '--no-precompile' flags.
|
||||
/// It throws a [StateError] if the command fails.
|
||||
Future getDependencies() async {
|
||||
const String cmd = "dart";
|
||||
|
||||
|
@ -147,6 +169,13 @@ class Build {
|
|||
}
|
||||
}
|
||||
|
||||
/// Compiles the source file to an executable.
|
||||
///
|
||||
/// [srcUri] is the URI of the source file to compile.
|
||||
/// [dstUri] is the URI where the compiled executable will be saved.
|
||||
///
|
||||
/// This method runs 'dart compile exe' with verbose output.
|
||||
/// It throws a [StateError] if the compilation fails.
|
||||
Future compile(Uri srcUri, Uri dstUri) async {
|
||||
final res = await Process.run(
|
||||
"dart",
|
||||
|
@ -173,6 +202,13 @@ class Build {
|
|||
print("${res.stdout}");
|
||||
}
|
||||
|
||||
/// Copies a package from source to destination.
|
||||
///
|
||||
/// [srcUri] is the URI of the source package.
|
||||
/// [dstUri] is the URI where the package will be copied.
|
||||
///
|
||||
/// This method creates the destination directory if it doesn't exist,
|
||||
/// copies the package contents, and handles Windows-specific file system issues.
|
||||
Future copyPackage(Uri srcUri, Uri dstUri) async {
|
||||
final dstDir = Directory.fromUri(dstUri);
|
||||
if (!dstDir.existsSync()) {
|
||||
|
@ -206,6 +242,12 @@ class Build {
|
|||
);
|
||||
}
|
||||
|
||||
/// Retrieves package information for a given compiler.
|
||||
///
|
||||
/// [compiler] is the compiler instance to get package information for.
|
||||
///
|
||||
/// This method uses reflection to find the source URI of the compiler
|
||||
/// and then retrieves the corresponding package information.
|
||||
Future<Package> _getPackageInfoForCompiler(Compiler compiler) async {
|
||||
final compilerUri = reflect(compiler).type.location!.sourceUri;
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import 'dart:io';
|
||||
import 'dart:mirrors';
|
||||
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
|
@ -17,8 +16,18 @@ import 'package:path/path.dart';
|
|||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
/// Configuration and context values used during [Build.execute].
|
||||
/// BuildContext provides configuration and context values used during the build process.
|
||||
/// It encapsulates information about the application being compiled, build artifacts,
|
||||
/// and provides utility methods for file and package management.
|
||||
class BuildContext {
|
||||
/// Constructs a new BuildContext instance.
|
||||
///
|
||||
/// [rootLibraryFileUri]: The URI of the root library file of the application being compiled.
|
||||
/// [buildDirectoryUri]: The URI of the directory where build artifacts will be stored.
|
||||
/// [executableUri]: The URI of the executable build product file.
|
||||
/// [source]: The source script for the executable.
|
||||
/// [environment]: Optional map of environment variables.
|
||||
/// [forTests]: Boolean flag indicating whether this context is for tests (includes dev dependencies).
|
||||
BuildContext(
|
||||
this.rootLibraryFileUri,
|
||||
this.buildDirectoryUri,
|
||||
|
@ -30,6 +39,12 @@ class BuildContext {
|
|||
analyzer = CodeAnalyzer(sourceApplicationDirectory.uri);
|
||||
}
|
||||
|
||||
/// Creates a BuildContext instance from a map of values.
|
||||
///
|
||||
/// This factory constructor allows for easy serialization and deserialization of BuildContext objects.
|
||||
///
|
||||
/// [map]: A map containing the necessary values to construct a BuildContext.
|
||||
/// Returns: A new BuildContext instance.
|
||||
factory BuildContext.fromMap(Map<String, dynamic> map) {
|
||||
return BuildContext(
|
||||
Uri.parse(map['rootLibraryFileUri']),
|
||||
|
@ -41,6 +56,10 @@ class BuildContext {
|
|||
);
|
||||
}
|
||||
|
||||
/// Returns a map representation of the BuildContext with safe-to-serialize values.
|
||||
///
|
||||
/// This getter is useful for serialization purposes, ensuring that all values are
|
||||
/// in a format that can be easily serialized (e.g., converting URIs to strings).
|
||||
Map<String, dynamic> get safeMap => {
|
||||
'rootLibraryFileUri': sourceLibraryFile.uri.toString(),
|
||||
'buildDirectoryUri': buildDirectoryUri.toString(),
|
||||
|
@ -50,41 +69,56 @@ class BuildContext {
|
|||
'forTests': forTests
|
||||
};
|
||||
|
||||
/// The CodeAnalyzer instance used for analyzing Dart code.
|
||||
late final CodeAnalyzer analyzer;
|
||||
|
||||
/// A [Uri] to the library file of the application to be compiled.
|
||||
/// The URI of the root library file of the application being compiled.
|
||||
final Uri rootLibraryFileUri;
|
||||
|
||||
/// A [Uri] to the executable build product file.
|
||||
/// The URI of the executable build product file.
|
||||
final Uri executableUri;
|
||||
|
||||
/// A [Uri] to directory where build artifacts are stored during the build process.
|
||||
/// The URI of the directory where build artifacts are stored during the build process.
|
||||
final Uri buildDirectoryUri;
|
||||
|
||||
/// The source script for the executable.
|
||||
final String source;
|
||||
|
||||
/// Whether dev dependencies of the application package are included in the dependencies of the compiled executable.
|
||||
/// Indicates whether dev dependencies of the application package should be included
|
||||
/// in the dependencies of the compiled executable.
|
||||
final bool forTests;
|
||||
|
||||
/// Cached PackageConfig instance.
|
||||
PackageConfig? _packageConfig;
|
||||
|
||||
/// Optional map of environment variables.
|
||||
final Map<String, String>? environment;
|
||||
|
||||
/// The [RuntimeContext] available during the build process.
|
||||
/// The current RuntimeContext, cast as a MirrorContext.
|
||||
///
|
||||
/// This getter provides access to the runtime context available during the build process.
|
||||
MirrorContext get context => RuntimeContext.current as MirrorContext;
|
||||
|
||||
/// The URI of the target script file.
|
||||
///
|
||||
/// If [forTests] is true, this will be a test file, otherwise it will be the main application file.
|
||||
Uri get targetScriptFileUri => forTests
|
||||
? getDirectory(buildDirectoryUri.resolve("test/"))
|
||||
.uri
|
||||
.resolve("main_test.dart")
|
||||
: buildDirectoryUri.resolve("main.dart");
|
||||
|
||||
/// The Pubspec of the source application.
|
||||
///
|
||||
/// This getter parses and returns the pubspec.yaml file of the application being compiled.
|
||||
Pubspec get sourceApplicationPubspec => Pubspec.parse(
|
||||
File.fromUri(sourceApplicationDirectory.uri.resolve("pubspec.yaml"))
|
||||
.readAsStringSync(),
|
||||
);
|
||||
|
||||
/// The pubspec of the source application as a map.
|
||||
///
|
||||
/// This getter loads and returns the pubspec.yaml file as a YAML map.
|
||||
Map<dynamic, dynamic> get sourceApplicationPubspecMap => loadYaml(
|
||||
File.fromUri(
|
||||
sourceApplicationDirectory.uri.resolve("pubspec.yaml"),
|
||||
|
@ -101,26 +135,32 @@ class BuildContext {
|
|||
/// The directory where build artifacts are stored.
|
||||
Directory get buildDirectory => getDirectory(buildDirectoryUri);
|
||||
|
||||
/// The generated runtime directory
|
||||
/// The generated runtime directory.
|
||||
Directory get buildRuntimeDirectory =>
|
||||
getDirectory(buildDirectoryUri.resolve("generated_runtime/"));
|
||||
|
||||
/// Directory for compiled packages
|
||||
/// Directory for compiled packages.
|
||||
Directory get buildPackagesDirectory =>
|
||||
getDirectory(buildDirectoryUri.resolve("packages/"));
|
||||
|
||||
/// Directory for compiled application
|
||||
/// Directory for the compiled application.
|
||||
Directory get buildApplicationDirectory => getDirectory(
|
||||
buildPackagesDirectory.uri.resolve("${sourceApplicationPubspec.name}/"),
|
||||
);
|
||||
|
||||
/// Gets dependency package location relative to [sourceApplicationDirectory].
|
||||
/// Gets the dependency package configuration relative to [sourceApplicationDirectory].
|
||||
///
|
||||
/// This getter lazily loads and caches the package configuration.
|
||||
/// Returns: A Future that resolves to a PackageConfig instance.
|
||||
Future<PackageConfig> get packageConfig async {
|
||||
return _packageConfig ??=
|
||||
(await findPackageConfig(sourceApplicationDirectory))!;
|
||||
}
|
||||
|
||||
/// Returns a [Directory] at [uri], creates it recursively if it doesn't exist.
|
||||
/// Returns a [Directory] at the specified [uri], creating it recursively if it doesn't exist.
|
||||
///
|
||||
/// [uri]: The URI of the directory.
|
||||
/// Returns: A Directory instance.
|
||||
Directory getDirectory(Uri uri) {
|
||||
final dir = Directory.fromUri(uri);
|
||||
if (!dir.existsSync()) {
|
||||
|
@ -129,7 +169,10 @@ class BuildContext {
|
|||
return dir;
|
||||
}
|
||||
|
||||
/// Returns a [File] at [uri], creates all parent directories recursively if necessary.
|
||||
/// Returns a [File] at the specified [uri], creating all parent directories recursively if necessary.
|
||||
///
|
||||
/// [uri]: The URI of the file.
|
||||
/// Returns: A File instance.
|
||||
File getFile(Uri uri) {
|
||||
final file = File.fromUri(uri);
|
||||
if (!file.parent.existsSync()) {
|
||||
|
@ -139,6 +182,11 @@ class BuildContext {
|
|||
return file;
|
||||
}
|
||||
|
||||
/// Retrieves a Package instance from a given URI.
|
||||
///
|
||||
/// [uri]: The URI to resolve to a package.
|
||||
/// Returns: A Future that resolves to a Package instance, or null if not found.
|
||||
/// Throws: ArgumentError if the URI is not absolute or a package URI.
|
||||
Future<Package?> getPackageFromUri(Uri? uri) async {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
|
@ -152,6 +200,13 @@ class BuildContext {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Retrieves import directives from a URI or source code.
|
||||
///
|
||||
/// [uri]: The URI of the file to analyze.
|
||||
/// [source]: The source code to analyze.
|
||||
/// [alsoImportOriginalFile]: Whether to include an import for the original file.
|
||||
/// Returns: A Future that resolves to a list of import directive strings.
|
||||
/// Throws: ArgumentError for invalid combinations of parameters.
|
||||
Future<List<String>> getImportDirectives({
|
||||
Uri? uri,
|
||||
String? source,
|
||||
|
@ -201,6 +256,10 @@ class BuildContext {
|
|||
return imports;
|
||||
}
|
||||
|
||||
/// Retrieves a ClassDeclaration from a given Type.
|
||||
///
|
||||
/// [type]: The Type to analyze.
|
||||
/// Returns: A Future that resolves to a ClassDeclaration, or null if not found.
|
||||
Future<ClassDeclaration?> getClassDeclarationFromType(Type type) async {
|
||||
final classMirror = reflectType(type);
|
||||
Uri uri = classMirror.location!.sourceUri;
|
||||
|
@ -214,6 +273,11 @@ class BuildContext {
|
|||
);
|
||||
}
|
||||
|
||||
/// Retrieves a FieldDeclaration from a ClassMirror and property name.
|
||||
///
|
||||
/// [type]: The ClassMirror to analyze.
|
||||
/// [propertyName]: The name of the property to find.
|
||||
/// Returns: A Future that resolves to a FieldDeclaration, or null if not found.
|
||||
Future<FieldDeclaration?> _getField(ClassMirror type, String propertyName) {
|
||||
return getClassDeclarationFromType(type.reflectedType).then((cd) {
|
||||
try {
|
||||
|
@ -229,6 +293,13 @@ class BuildContext {
|
|||
});
|
||||
}
|
||||
|
||||
/// Retrieves annotations from a field of a given Type.
|
||||
///
|
||||
/// This method searches for the field in the given type and its superclasses.
|
||||
///
|
||||
/// [type1]: The Type to analyze.
|
||||
/// [propertyName]: The name of the property to find.
|
||||
/// Returns: A Future that resolves to a list of Annotations.
|
||||
Future<List<Annotation>> getAnnotationsFromField(
|
||||
Type type1,
|
||||
String propertyName,
|
||||
|
|
|
@ -8,20 +8,31 @@
|
|||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:protevus_isolate/isolate.dart';
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
import 'package:io/io.dart';
|
||||
|
||||
/// A class that represents an executable build process.
|
||||
///
|
||||
/// This class extends the [Executable] class and is responsible for
|
||||
/// executing a build process based on the provided build context.
|
||||
class BuildExecutable extends Executable {
|
||||
/// Constructs a new [BuildExecutable] instance.
|
||||
///
|
||||
/// [message] is a map containing the build context information.
|
||||
BuildExecutable(Map<String, dynamic> message) : super(message) {
|
||||
context = BuildContext.fromMap(message);
|
||||
}
|
||||
|
||||
/// The build context for this executable.
|
||||
late final BuildContext context;
|
||||
|
||||
/// Executes the build process.
|
||||
///
|
||||
/// This method creates a new [Build] instance with the current context
|
||||
/// and calls its execute method.
|
||||
@override
|
||||
Future execute() async {
|
||||
final build = Build(context);
|
||||
|
@ -29,21 +40,33 @@ class BuildExecutable extends Executable {
|
|||
}
|
||||
}
|
||||
|
||||
/// A class that manages the build process for non-mirrored builds.
|
||||
class BuildManager {
|
||||
/// Creates a new build manager to compile a non-mirrored build.
|
||||
/// Creates a new [BuildManager] instance.
|
||||
///
|
||||
/// [context] is the [BuildContext] for this build manager.
|
||||
BuildManager(this.context);
|
||||
|
||||
/// The build context for this manager.
|
||||
final BuildContext context;
|
||||
|
||||
/// Gets the URI of the source directory.
|
||||
Uri get sourceDirectoryUri => context.sourceApplicationDirectory.uri;
|
||||
|
||||
/// Performs the build process.
|
||||
///
|
||||
/// This method handles the following steps:
|
||||
/// 1. Creates the build directory if it doesn't exist.
|
||||
/// 2. Creates a temporary copy of the script file with the main function stripped.
|
||||
/// 3. Analyzes the script file and removes all main functions.
|
||||
/// 4. Copies the 'not_tests' directory if it exists.
|
||||
/// 5. Runs the build executable in an isolate.
|
||||
Future build() async {
|
||||
if (!context.buildDirectory.existsSync()) {
|
||||
context.buildDirectory.createSync();
|
||||
}
|
||||
|
||||
// Here is where we need to provide a temporary copy of the script file with the main function stripped;
|
||||
// this is because when the RuntimeGenerator loads, it needs Mirror access to any declarations in this file
|
||||
// Create a temporary copy of the script file with the main function stripped
|
||||
var scriptSource = context.source;
|
||||
final strippedScriptFile = File.fromUri(context.targetScriptFileUri)
|
||||
..writeAsStringSync(scriptSource);
|
||||
|
@ -52,6 +75,7 @@ class BuildManager {
|
|||
final parsedUnit = analyzerContext.currentSession
|
||||
.getParsedUnit(analyzer.path) as ParsedUnitResult;
|
||||
|
||||
// Find and remove all main functions
|
||||
final mainFunctions = parsedUnit.unit.declarations
|
||||
.whereType<FunctionDeclaration>()
|
||||
.where((f) => f.name.value() == "main")
|
||||
|
@ -63,12 +87,14 @@ class BuildManager {
|
|||
|
||||
strippedScriptFile.writeAsStringSync(scriptSource);
|
||||
|
||||
// Copy the 'not_tests' directory if it exists
|
||||
try {
|
||||
await copyPath(
|
||||
context.sourceApplicationDirectory.uri.resolve('test/not_tests').path,
|
||||
context.buildDirectoryUri.resolve('not_tests').path);
|
||||
} catch (_) {}
|
||||
|
||||
// Run the build executable in an isolate
|
||||
await IsolateExecutor.run(
|
||||
BuildExecutable(context.safeMap),
|
||||
packageConfigURI:
|
||||
|
@ -81,6 +107,9 @@ class BuildManager {
|
|||
);
|
||||
}
|
||||
|
||||
/// Cleans up the build directory.
|
||||
///
|
||||
/// This method deletes the build directory and its contents if it exists.
|
||||
Future clean() async {
|
||||
if (context.buildDirectory.existsSync()) {
|
||||
context.buildDirectory.deleteSync(recursive: true);
|
||||
|
|
|
@ -8,32 +8,70 @@
|
|||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
|
||||
abstract class Compiler {
|
||||
/// Modifies a package on the filesystem in order to remove dart:mirrors from the package.
|
||||
/// An abstract class that defines the interface for compilers in the Protevus Platform.
|
||||
///
|
||||
/// A copy of this compiler's package will be written to [destinationDirectory].
|
||||
/// This method is overridden to modify the contents of that directory
|
||||
/// to remove all uses of dart:mirrors.
|
||||
/// This class provides methods for modifying packages, compiling runtime objects,
|
||||
/// and handling post-generation tasks. Implementations of this class are used to
|
||||
/// remove dart:mirrors usage from packages and prepare them for runtime execution.
|
||||
abstract class Compiler {
|
||||
/// Modifies a package on the filesystem to remove dart:mirrors usage.
|
||||
///
|
||||
/// This method creates a copy of the compiler's package in the [destinationDirectory]
|
||||
/// and modifies its contents to remove all uses of dart:mirrors. This is crucial for
|
||||
/// preparing packages for environments where reflection is not available or desired.
|
||||
///
|
||||
/// Packages should export their [Compiler] in their main library file and only
|
||||
/// import mirrors in files directly or transitively imported by the Compiler file.
|
||||
/// This method should remove that export statement and therefore remove all transitive mirror imports.
|
||||
///
|
||||
/// [destinationDirectory] The directory where the modified package will be written.
|
||||
void deflectPackage(Directory destinationDirectory);
|
||||
|
||||
/// Returns a map of runtime objects that can be used at runtime while running in mirrored mode.
|
||||
/// Compiles and returns a map of runtime objects for use in mirrored mode.
|
||||
///
|
||||
/// This method is responsible for creating runtime representations of objects
|
||||
/// that can be used when the application is running in mirrored mode.
|
||||
///
|
||||
/// [context] The [MirrorContext] providing reflection capabilities for compilation.
|
||||
///
|
||||
/// Returns a [Map] where keys are String identifiers and values are compiled Objects.
|
||||
Map<String, Object> compile(MirrorContext context);
|
||||
|
||||
/// A hook method called after package generation is complete.
|
||||
///
|
||||
/// This method can be overridden to perform any necessary cleanup or additional
|
||||
/// tasks after the package has been generated.
|
||||
///
|
||||
/// [context] The [BuildContext] containing information about the build process.
|
||||
void didFinishPackageGeneration(BuildContext context) {}
|
||||
|
||||
/// Returns a list of URIs that need to be resolved during the build process.
|
||||
///
|
||||
/// This method can be overridden to specify additional URIs that should be
|
||||
/// resolved as part of the compilation process.
|
||||
///
|
||||
/// [context] The [BuildContext] containing information about the build process.
|
||||
///
|
||||
/// Returns a [List] of [Uri] objects to be resolved.
|
||||
List<Uri> getUrisToResolve(BuildContext context) => [];
|
||||
}
|
||||
|
||||
/// Runtimes that generate source code implement this method.
|
||||
/// An abstract class for compilers that generate source code.
|
||||
///
|
||||
/// This class extends the functionality of [Compiler] to include
|
||||
/// the ability to generate source code that represents the runtime behavior.
|
||||
abstract class SourceCompiler {
|
||||
/// The source code, including directives, that declare a class that is equivalent in behavior to this runtime.
|
||||
/// Generates source code that declares a class equivalent in behavior to this runtime.
|
||||
///
|
||||
/// This method should be implemented to produce a string of Dart source code
|
||||
/// that includes all necessary directives and class declarations to replicate
|
||||
/// the behavior of the runtime in a static context.
|
||||
///
|
||||
/// [ctx] The [BuildContext] containing information about the build process.
|
||||
///
|
||||
/// Returns a [Future] that resolves to a [String] containing the generated source code.
|
||||
Future<String> compile(BuildContext ctx) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
|
|
@ -10,20 +10,26 @@
|
|||
import 'package:protevus_runtime/runtime.dart';
|
||||
|
||||
/// Contextual values used during runtime.
|
||||
///
|
||||
/// This abstract class defines the structure for runtime contexts in the Protevus Platform.
|
||||
/// It provides access to runtime objects and coercion functionality.
|
||||
abstract class RuntimeContext {
|
||||
/// The current [RuntimeContext] available to the executing application.
|
||||
///
|
||||
/// Is either a `MirrorContext` or a `GeneratedContext`,
|
||||
/// This static property holds either a `MirrorContext` or a `GeneratedContext`,
|
||||
/// depending on the execution type.
|
||||
static final RuntimeContext current = instance;
|
||||
|
||||
/// The runtimes available to the executing application.
|
||||
///
|
||||
/// This property stores a collection of runtime objects that can be accessed
|
||||
/// during the application's execution.
|
||||
late RuntimeCollection runtimes;
|
||||
|
||||
/// Gets a runtime object for [type].
|
||||
/// Gets a runtime object for the specified [type].
|
||||
///
|
||||
/// Callers typically invoke this method, passing their [runtimeType]
|
||||
/// in order to retrieve their runtime object.
|
||||
/// Callers typically invoke this operator, passing their [runtimeType]
|
||||
/// to retrieve their runtime object.
|
||||
///
|
||||
/// It is important to note that a runtime object must exist for every
|
||||
/// class that extends a class that has a runtime. Use `MirrorContext.getSubclassesOf` when compiling.
|
||||
|
@ -31,18 +37,44 @@ abstract class RuntimeContext {
|
|||
/// In other words, if the type `Base` has a runtime and the type `Subclass` extends `Base`,
|
||||
/// `Subclass` must also have a runtime. The runtime objects for both `Subclass` and `Base`
|
||||
/// must be the same type.
|
||||
///
|
||||
/// @param type The Type to retrieve the runtime object for.
|
||||
/// @return The runtime object associated with the specified type.
|
||||
dynamic operator [](Type type) => runtimes[type];
|
||||
|
||||
/// Coerces the given [input] to the specified type [T].
|
||||
///
|
||||
/// This method is used to convert or cast the input to the desired type.
|
||||
///
|
||||
/// @param input The input value to be coerced.
|
||||
/// @return The coerced value of type [T].
|
||||
T coerce<T>(dynamic input);
|
||||
}
|
||||
|
||||
/// A collection of runtime objects indexed by type names.
|
||||
///
|
||||
/// This class provides a way to store and retrieve runtime objects
|
||||
/// associated with specific types.
|
||||
class RuntimeCollection {
|
||||
/// Creates a new [RuntimeCollection] with the given [map].
|
||||
///
|
||||
/// @param map A map where keys are type names and values are runtime objects.
|
||||
RuntimeCollection(this.map);
|
||||
|
||||
/// The underlying map storing runtime objects.
|
||||
final Map<String, Object> map;
|
||||
|
||||
/// Returns an iterable of all runtime objects in the collection.
|
||||
Iterable<Object> get iterable => map.values;
|
||||
|
||||
/// Retrieves the runtime object for the specified type [t].
|
||||
///
|
||||
/// This operator first attempts to find an exact match for the type name.
|
||||
/// If not found, it tries to match a generic type by removing type parameters.
|
||||
///
|
||||
/// @param t The Type to retrieve the runtime object for.
|
||||
/// @return The runtime object associated with the specified type.
|
||||
/// @throws ArgumentError if no runtime object is found for the given type.
|
||||
Object operator [](Type t) {
|
||||
//todo: optimize by keeping a cache where keys are of type [Type] to avoid the
|
||||
// expensive indexOf and substring calls in this method
|
||||
|
@ -67,10 +99,10 @@ class RuntimeCollection {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prevents a type from being compiled when it otherwise would be.
|
||||
/// An annotation to prevent a type from being compiled when it otherwise would be.
|
||||
///
|
||||
/// Annotate a type with the const instance of this type to prevent its
|
||||
/// compilation.
|
||||
/// Use this annotation on a type to exclude it from compilation.
|
||||
class PreventCompilation {
|
||||
/// Creates a constant instance of [PreventCompilation].
|
||||
const PreventCompilation();
|
||||
}
|
||||
|
|
|
@ -7,13 +7,38 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/// Exception thrown when type coercion fails.
|
||||
///
|
||||
/// This exception is used to indicate that an attempt to convert a value
|
||||
/// from one type to another has failed. It provides information about
|
||||
/// the expected type and the actual type of the value.
|
||||
class TypeCoercionException implements Exception {
|
||||
/// Creates a new [TypeCoercionException].
|
||||
///
|
||||
/// [expectedType] is the type that was expected for the conversion.
|
||||
/// [actualType] is the actual type of the value that couldn't be converted.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// throw TypeCoercionException(String, int);
|
||||
/// ```
|
||||
TypeCoercionException(this.expectedType, this.actualType);
|
||||
|
||||
/// The type that was expected for the conversion.
|
||||
final Type expectedType;
|
||||
|
||||
/// The actual type of the value that couldn't be converted.
|
||||
final Type actualType;
|
||||
|
||||
@override
|
||||
|
||||
/// Returns a string representation of the exception.
|
||||
///
|
||||
/// The returned string includes both the expected type and the actual type
|
||||
/// to provide clear information about the nature of the coercion failure.
|
||||
///
|
||||
/// Returns:
|
||||
/// A string describing the type coercion exception.
|
||||
String toString() {
|
||||
return "input is not expected type '$expectedType' (input is '$actualType')";
|
||||
}
|
||||
|
|
|
@ -10,16 +10,32 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
/// Token used to replace directives in the loader shell.
|
||||
const String _directiveToken = "___DIRECTIVES___";
|
||||
|
||||
/// Token used to replace assignments in the loader shell.
|
||||
const String _assignmentToken = "___ASSIGNMENTS___";
|
||||
|
||||
/// A class responsible for generating runtime code.
|
||||
///
|
||||
/// This class allows adding runtime elements and writing them to a specified directory.
|
||||
class RuntimeGenerator {
|
||||
/// List of runtime elements to be generated.
|
||||
final _elements = <_RuntimeElement>[];
|
||||
|
||||
/// Adds a new runtime element to the generator.
|
||||
///
|
||||
/// [name] is the name of the runtime element.
|
||||
/// [source] is the source code of the runtime element.
|
||||
void addRuntime({required String name, required String source}) {
|
||||
_elements.add(_RuntimeElement(name, source));
|
||||
}
|
||||
|
||||
/// Writes the generated runtime code to the specified directory.
|
||||
///
|
||||
/// [directoryUri] is the URI of the directory where the code will be written.
|
||||
/// This method creates necessary directories, writes the library file,
|
||||
/// pubspec file, and individual runtime element files.
|
||||
Future<void> writeTo(Uri directoryUri) async {
|
||||
final dir = Directory.fromUri(directoryUri);
|
||||
final libDir = Directory.fromUri(dir.uri.resolve("lib/"));
|
||||
|
@ -48,6 +64,7 @@ class RuntimeGenerator {
|
|||
});
|
||||
}
|
||||
|
||||
/// Returns the content of the pubspec.yaml file as a string.
|
||||
String get pubspecSource => """
|
||||
name: generated_runtime
|
||||
description: A runtime generated by package:conduit_runtime
|
||||
|
@ -57,6 +74,9 @@ environment:
|
|||
sdk: '>=3.4.0 <4.0.0'
|
||||
""";
|
||||
|
||||
/// Returns the shell of the loader file as a string.
|
||||
///
|
||||
/// This shell contains placeholders for directives and assignments.
|
||||
String get _loaderShell => """
|
||||
import 'package:conduit_runtime/runtime.dart';
|
||||
import 'package:conduit_runtime/slow_coerce.dart' as runtime_cast;
|
||||
|
@ -80,12 +100,17 @@ class GeneratedContext extends RuntimeContext {
|
|||
}
|
||||
""";
|
||||
|
||||
/// Returns the complete source of the loader file.
|
||||
///
|
||||
/// This method replaces the directive and assignment tokens in the loader shell
|
||||
/// with the actual directives and assignments.
|
||||
String get loaderSource {
|
||||
return _loaderShell
|
||||
.replaceFirst(_directiveToken, _directives)
|
||||
.replaceFirst(_assignmentToken, _assignments);
|
||||
}
|
||||
|
||||
/// Generates the import directives for all runtime elements.
|
||||
String get _directives {
|
||||
final buf = StringBuffer();
|
||||
|
||||
|
@ -98,6 +123,7 @@ class GeneratedContext extends RuntimeContext {
|
|||
return buf.toString();
|
||||
}
|
||||
|
||||
/// Generates the assignments for all runtime elements.
|
||||
String get _assignments {
|
||||
final buf = StringBuffer();
|
||||
|
||||
|
@ -109,14 +135,24 @@ class GeneratedContext extends RuntimeContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// A class representing a single runtime element.
|
||||
class _RuntimeElement {
|
||||
/// Creates a new runtime element.
|
||||
///
|
||||
/// [typeName] is the name of the type for this runtime element.
|
||||
/// [source] is the source code of this runtime element.
|
||||
_RuntimeElement(this.typeName, this.source);
|
||||
|
||||
/// The name of the type for this runtime element.
|
||||
final String typeName;
|
||||
|
||||
/// The source code of this runtime element.
|
||||
final String source;
|
||||
|
||||
/// Returns the relative URI for this runtime element's file.
|
||||
Uri get relativeUri => Uri.file("${typeName.toLowerCase()}.dart");
|
||||
|
||||
/// Returns the import alias for this runtime element.
|
||||
String get importAlias {
|
||||
return "g_${typeName.toLowerCase()}";
|
||||
}
|
||||
|
|
|
@ -8,9 +8,23 @@
|
|||
*/
|
||||
|
||||
import 'dart:mirrors';
|
||||
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
|
||||
/// Attempts to cast an object to a specified type at runtime.
|
||||
///
|
||||
/// This function uses Dart's mirror system to perform runtime type checking
|
||||
/// and casting. It handles casting to List and Map types, including their
|
||||
/// generic type arguments.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [object]: The object to be cast.
|
||||
/// - [intoType]: A [TypeMirror] representing the type to cast into.
|
||||
///
|
||||
/// Returns:
|
||||
/// The object cast to the specified type.
|
||||
///
|
||||
/// Throws:
|
||||
/// - [TypeCoercionException] if the casting fails.
|
||||
Object runtimeCast(Object object, TypeMirror intoType) {
|
||||
final exceptionToThrow =
|
||||
TypeCoercionException(intoType.reflectedType, object.runtimeType);
|
||||
|
@ -51,6 +65,16 @@ Object runtimeCast(Object object, TypeMirror intoType) {
|
|||
throw exceptionToThrow;
|
||||
}
|
||||
|
||||
/// Determines if a given type is fully primitive.
|
||||
///
|
||||
/// A type is considered fully primitive if it's a basic type (num, String, bool),
|
||||
/// dynamic, or a collection (List, Map) where all nested types are also primitive.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [type]: A [TypeMirror] representing the type to check.
|
||||
///
|
||||
/// Returns:
|
||||
/// true if the type is fully primitive, false otherwise.
|
||||
bool isTypeFullyPrimitive(TypeMirror type) {
|
||||
if (type == reflectType(dynamic)) {
|
||||
return true;
|
||||
|
|
|
@ -8,12 +8,19 @@
|
|||
*/
|
||||
|
||||
import 'dart:mirrors';
|
||||
|
||||
import 'package:protevus_runtime/runtime.dart';
|
||||
|
||||
/// Global instance of the MirrorContext.
|
||||
RuntimeContext instance = MirrorContext._();
|
||||
|
||||
/// A runtime context implementation using Dart's mirror system.
|
||||
///
|
||||
/// This class provides runtime type information and compilation capabilities
|
||||
/// using reflection.
|
||||
class MirrorContext extends RuntimeContext {
|
||||
/// Private constructor to ensure singleton instance.
|
||||
///
|
||||
/// Initializes the context by compiling all available runtimes.
|
||||
MirrorContext._() {
|
||||
final m = <String, Object>{};
|
||||
|
||||
|
@ -31,6 +38,9 @@ class MirrorContext extends RuntimeContext {
|
|||
runtimes = RuntimeCollection(m);
|
||||
}
|
||||
|
||||
/// List of all class mirrors in the current mirror system.
|
||||
///
|
||||
/// Excludes classes marked with @PreventCompilation.
|
||||
final List<ClassMirror> types = currentMirrorSystem()
|
||||
.libraries
|
||||
.values
|
||||
|
@ -40,6 +50,9 @@ class MirrorContext extends RuntimeContext {
|
|||
.where((cm) => firstMetadataOfType<PreventCompilation>(cm) == null)
|
||||
.toList();
|
||||
|
||||
/// List of all available compilers.
|
||||
///
|
||||
/// Returns instances of non-abstract classes that are subclasses of Compiler.
|
||||
List<Compiler> get compilers {
|
||||
return types
|
||||
.where((b) => b.isSubclassOf(reflectClass(Compiler)) && !b.isAbstract)
|
||||
|
@ -47,6 +60,10 @@ class MirrorContext extends RuntimeContext {
|
|||
.toList();
|
||||
}
|
||||
|
||||
/// Retrieves all non-abstract subclasses of a given type.
|
||||
///
|
||||
/// [type] The base type to find subclasses of.
|
||||
/// Returns a list of ClassMirror objects representing the subclasses.
|
||||
List<ClassMirror> getSubclassesOf(Type type) {
|
||||
final mirror = reflectClass(type);
|
||||
return types.where((decl) {
|
||||
|
@ -68,6 +85,13 @@ class MirrorContext extends RuntimeContext {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
/// Coerces an input to a specified type T.
|
||||
///
|
||||
/// Attempts to cast the input directly, and if that fails,
|
||||
/// uses runtime casting.
|
||||
///
|
||||
/// [input] The object to be coerced.
|
||||
/// Returns the coerced object of type T.
|
||||
@override
|
||||
T coerce<T>(dynamic input) {
|
||||
try {
|
||||
|
@ -78,6 +102,11 @@ class MirrorContext extends RuntimeContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Retrieves the first metadata annotation of a specific type from a declaration.
|
||||
///
|
||||
/// [dm] The DeclarationMirror to search for metadata.
|
||||
/// [dynamicType] Optional TypeMirror to use instead of T.
|
||||
/// Returns the first metadata of type T, or null if not found.
|
||||
T? firstMetadataOfType<T>(DeclarationMirror dm, {TypeMirror? dynamicType}) {
|
||||
final tMirror = dynamicType ?? reflectType(T);
|
||||
try {
|
||||
|
|
Loading…
Reference in a new issue