Compare commits

...

3 commits

Author SHA1 Message Date
Patrick Stewart
ea26e024ce update: updating files with detailed comments 2024-09-08 23:09:22 -07:00
Patrick Stewart
5fd57e1ebd update: updating files with detailed comments 2024-09-08 23:09:16 -07:00
Patrick Stewart
0a3d903320 update: updating files with detailed comments 2024-09-08 23:08:53 -07:00
38 changed files with 1575 additions and 104 deletions

View file

@ -1,3 +1,26 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/// This library exports various components and utilities for handling HTTP requests and responses,
/// including controllers, request/response processing, routing, and serialization.
///
/// It provides a comprehensive set of tools for building web applications and APIs, including:
/// - Request and response handling
/// - Body decoding and encoding
/// - Caching and CORS policies
/// - File handling
/// - Database object controllers
/// - Resource controllers with bindings and scopes
/// - Routing and request path processing
/// - Serialization utilities
library;
export 'src/body_decoder.dart'; export 'src/body_decoder.dart';
export 'src/cache_policy.dart'; export 'src/cache_policy.dart';
export 'src/controller.dart'; export 'src/controller.dart';

View file

@ -1,14 +1,26 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
import 'package:protevus_runtime/runtime.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 { abstract class BodyDecoder {
/// Creates a new [BodyDecoder] instance.
///
/// [bodyByteStream] is the stream of bytes to be decoded.
BodyDecoder(Stream<List<int>> bodyByteStream) BodyDecoder(Stream<List<int>> bodyByteStream)
: _originalByteStream = bodyByteStream; : _originalByteStream = bodyByteStream;
@ -67,8 +79,13 @@ abstract class BodyDecoder {
return _bytes; return _bytes;
} }
/// The original byte stream to be decoded.
final Stream<List<int>> _originalByteStream; final Stream<List<int>> _originalByteStream;
/// The decoded data after processing.
dynamic _decodedData; dynamic _decodedData;
/// The original bytes, if retained.
List<int>? _bytes; List<int>? _bytes;
/// Decodes this object's bytes as [T]. /// Decodes this object's bytes as [T].
@ -127,6 +144,9 @@ abstract class BodyDecoder {
return _cast<T>(_decodedData); 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) { T _cast<T>(dynamic body) {
try { try {
return RuntimeContext.current.coerce<T>(body); 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 { Future<List<int>> _readBytes(Stream<List<int>> stream) async {
return (await stream.toList()).expand((e) => e).toList(); return (await stream.toList()).expand((e) => e).toList();
} }

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Instances of this type provide configuration for the 'Cache-Control' header. /// 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 /// Policies applied to [Response.cachePolicy] will add the appropriate
/// headers to that response. See properties for definitions of arguments /// headers to that response. See properties for definitions of arguments
/// to this constructor. /// 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({ const CachePolicy({
this.preventIntermediateProxyCaching = false, this.preventIntermediateProxyCaching = false,
this.preventCaching = false, this.preventCaching = false,
@ -40,6 +54,9 @@ class CachePolicy {
/// Constructs a header value configured from this instance. /// Constructs a header value configured from this instance.
/// ///
/// This value is used for the 'Cache-Control' header. /// 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 { String get headerValue {
if (preventCaching) { if (preventCaching) {
return "no-cache, no-store"; return "no-cache, no-store";

View file

@ -1,6 +1,14 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.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) { void applyCORSHeadersIfNecessary(Request req, Response resp) {
if (req.isCORSRequest && !req.isPreflightRequest) { if (req.isCORSRequest && !req.isPreflightRequest) {
final lastPolicyController = _lastController; 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 @override
Map<String, APIPath> documentPaths(APIDocumentContext context) => Map<String, APIPath> documentPaths(APIDocumentContext context) =>
nextController?.documentPaths(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 @override
Map<String, APIOperation> documentOperations( Map<String, APIOperation> documentOperations(
APIDocumentContext context, APIDocumentContext context,
@ -292,10 +330,21 @@ abstract class Controller
return nextController!.documentOperations(context, route, path); 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 @override
void documentComponents(APIDocumentContext context) => void documentComponents(APIDocumentContext context) =>
nextController?.documentComponents(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 { Future? _handlePreflightRequest(Request req) async {
Controller controllerToDictatePolicy; Controller controllerToDictatePolicy;
try { try {
@ -327,6 +376,10 @@ abstract class Controller
return controllerToDictatePolicy.receive(req); 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( Future _sendResponse(
Request request, Request request,
Response response, { Response response, {
@ -340,6 +393,9 @@ abstract class Controller
return request.respond(response); 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 get _lastController {
Controller controller = this; Controller controller = this;
while (controller.nextController != null) { 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() @PreventCompilation()
class _ControllerRecycler<T> extends Controller { class _ControllerRecycler<T> extends Controller {
_ControllerRecycler(this.generator, Recyclable<T> instance) { _ControllerRecycler(this.generator, Recyclable<T> instance) {
@ -356,14 +415,21 @@ class _ControllerRecycler<T> extends Controller {
nextInstanceToReceive = instance; nextInstanceToReceive = instance;
} }
/// Function to generate new instances of the recyclable controller.
Controller Function() generator; Controller Function() generator;
/// Override for the CORS policy.
CORSPolicy? policyOverride; CORSPolicy? policyOverride;
/// State to be recycled between instances.
T? recycleState; T? recycleState;
Recyclable<T>? _nextInstanceToReceive; Recyclable<T>? _nextInstanceToReceive;
/// The next instance to receive requests.
Recyclable<T>? get nextInstanceToReceive => _nextInstanceToReceive; Recyclable<T>? get nextInstanceToReceive => _nextInstanceToReceive;
/// Sets the next instance to receive requests and initializes it.
set nextInstanceToReceive(Recyclable<T>? instance) { set nextInstanceToReceive(Recyclable<T>? instance) {
_nextInstanceToReceive = instance; _nextInstanceToReceive = instance;
instance?.restore(recycleState); 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 @override
CORSPolicy? get policy { CORSPolicy? get policy {
return nextInstanceToReceive?.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 @override
set policy(CORSPolicy? p) { set policy(CORSPolicy? p) {
policyOverride = 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 @override
Linkable link(Controller Function() instantiator) { Linkable link(Controller Function() instantiator) {
final c = super.link(instantiator); final c = super.link(instantiator);
@ -390,6 +481,16 @@ class _ControllerRecycler<T> extends Controller {
return c; 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 @override
Linkable? linkFunction( Linkable? linkFunction(
FutureOr<RequestOrResponse?> Function(Request request) handle, FutureOr<RequestOrResponse?> Function(Request request) handle,
@ -399,6 +500,22 @@ class _ControllerRecycler<T> extends Controller {
return c; 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 @override
Future? receive(Request req) { Future? receive(Request req) {
final next = nextInstanceToReceive; final next = nextInstanceToReceive;
@ -406,11 +523,28 @@ class _ControllerRecycler<T> extends Controller {
return next!.receive(req); 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 @override
FutureOr<RequestOrResponse> handle(Request request) { FutureOr<RequestOrResponse> handle(Request request) {
throw StateError("_ControllerRecycler invoked handle. This is a bug."); 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 @override
void didAddToChannel() { void didAddToChannel() {
// don't call super, since nextInstanceToReceive's nextController is set to the same instance, // 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(); 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 @override
void documentComponents(APIDocumentContext components) => void documentComponents(APIDocumentContext components) =>
nextInstanceToReceive?.documentComponents(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 @override
Map<String, APIPath> documentPaths(APIDocumentContext components) => Map<String, APIPath> documentPaths(APIDocumentContext components) =>
nextInstanceToReceive?.documentPaths(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 @override
Map<String, APIOperation> documentOperations( Map<String, APIOperation> documentOperations(
APIDocumentContext components, APIDocumentContext components,
@ -435,17 +612,49 @@ class _ControllerRecycler<T> extends Controller {
nextInstanceToReceive?.documentOperations(components, route, path) ?? {}; nextInstanceToReceive?.documentOperations(components, route, path) ?? {};
} }
/// A controller that wraps a function to handle requests.
@PreventCompilation() @PreventCompilation()
class _FunctionController extends Controller { class _FunctionController extends Controller {
_FunctionController(this._handler); _FunctionController(this._handler);
/// The function that handles requests.
final FutureOr<RequestOrResponse?> Function(Request) _handler; 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 @override
FutureOr<RequestOrResponse?> handle(Request request) { FutureOr<RequestOrResponse?> handle(Request request) {
return _handler(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 @override
Map<String, APIOperation> documentOperations( Map<String, APIOperation> documentOperations(
APIDocumentContext context, APIDocumentContext context,
@ -460,8 +669,11 @@ class _FunctionController extends Controller {
} }
} }
/// Abstract class representing the runtime of a controller.
abstract class ControllerRuntime { abstract class ControllerRuntime {
/// Whether the controller is mutable.
bool get isMutable; bool get isMutable;
/// The resource controller runtime, if applicable.
ResourceControllerRuntime? get resourceController; ResourceControllerRuntime? get resourceController;
} }

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:io'; import 'dart:io';
import 'package:protevus_http/http.dart'; 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. /// 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. /// Application-wide defaults can be managed by modifying [defaultPolicy] in a [ApplicationChannel]'s constructor.
///
class CORSPolicy { class CORSPolicy {
/// Create a new instance of [CORSPolicy]. /// Create a new instance of [CORSPolicy].
/// ///
@ -26,6 +34,7 @@ class CORSPolicy {
cacheInSeconds = def.cacheInSeconds; cacheInSeconds = def.cacheInSeconds;
} }
/// Creates a new instance of [CORSPolicy] with default values.
CORSPolicy._defaults() { CORSPolicy._defaults() {
allowedOrigins = ["*"]; allowedOrigins = ["*"];
allowCredentials = true; allowCredentials = true;
@ -50,6 +59,7 @@ class CORSPolicy {
return _defaultPolicy ??= CORSPolicy._defaults(); return _defaultPolicy ??= CORSPolicy._defaults();
} }
/// Internal storage for the default policy.
static CORSPolicy? _defaultPolicy; static CORSPolicy? _defaultPolicy;
/// List of 'Simple' CORS headers. /// List of 'Simple' CORS headers.
@ -88,8 +98,6 @@ class CORSPolicy {
/// Which response headers to expose to the client. /// 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'. /// Defaults to empty. In the specification (http://www.w3.org/TR/cors/), this is 'list of exposed headers'.
///
///
late List<String> exposedResponseHeaders; late List<String> exposedResponseHeaders;
/// Which HTTP methods are allowed. /// 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 /// This will add Access-Control-Allow-Origin, Access-Control-Expose-Headers and Access-Control-Allow-Credentials
/// depending on the this policy. /// depending on the this policy.
///
/// [request] The incoming request.
/// Returns a map of HTTP headers.
Map<String, dynamic> headersForRequest(Request request) { Map<String, dynamic> headersForRequest(Request request) {
final origin = request.raw.headers.value("origin"); 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], /// Will return true if [allowedOrigins] contains the case-sensitive Origin of the [request],
/// or that [allowedOrigins] contains *. /// or that [allowedOrigins] contains *.
/// This method is invoked internally by [Controller]s that have a [Controller.policy]. /// 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) { bool isRequestOriginAllowed(HttpRequest request) {
if (allowedOrigins.contains("*")) { if (allowedOrigins.contains("*")) {
return true; 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. /// 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]. /// 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) { bool validatePreflightRequest(HttpRequest request) {
if (!isRequestOriginAllowed(request)) { if (!isRequestOriginAllowed(request)) {
return false; return false;
@ -181,6 +198,9 @@ class CORSPolicy {
/// Contains the Access-Control-Allow-* headers for a CORS preflight request according /// Contains the Access-Control-Allow-* headers for a CORS preflight request according
/// to this policy. /// to this policy.
/// This method is invoked internally by [Controller]s that have a [Controller.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) { Response preflightResponse(Request req) {
final headers = { final headers = {
"Access-Control-Allow-Origin": req.raw.headers.value("origin"), "Access-Control-Allow-Origin": req.raw.headers.value("origin"),

View file

@ -1,11 +1,20 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart'; import 'package:protevus_openapi/v3.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
/// A typedef for a function that handles file controller operations.
typedef FileControllerClosure = FutureOr<Response> Function( typedef FileControllerClosure = FutureOr<Response> Function(
FileController controller, FileController controller,
Request req, Request req,
@ -48,6 +57,7 @@ class FileController extends Controller {
}) : _servingDirectory = Uri.directory(pathOfDirectoryToServe), }) : _servingDirectory = Uri.directory(pathOfDirectoryToServe),
_onFileNotFound = onFileNotFound; _onFileNotFound = onFileNotFound;
/// A map of default file extensions to their corresponding ContentTypes.
static final Map<String, ContentType> _defaultExtensionMap = { static final Map<String, ContentType> _defaultExtensionMap = {
/* Web content */ /* Web content */
"html": ContentType("text", "html", charset: "utf-8"), "html": ContentType("text", "html", charset: "utf-8"),
@ -80,9 +90,16 @@ class FileController extends Controller {
"otf": ContentType("font", "otf"), "otf": ContentType("font", "otf"),
}; };
/// A map of file extensions to their corresponding ContentTypes.
final Map<String, ContentType> _extensionMap = Map.from(_defaultExtensionMap); final Map<String, ContentType> _extensionMap = Map.from(_defaultExtensionMap);
/// A list of policy pairs for caching.
final List<_PolicyPair?> _policyPairs = []; final List<_PolicyPair?> _policyPairs = [];
/// The URI of the directory being served.
final Uri _servingDirectory; final Uri _servingDirectory;
/// A function to handle file not found errors.
final FutureOr<Response> Function( final FutureOr<Response> Function(
FileController, FileController,
Request, Request,
@ -155,6 +172,7 @@ class FileController extends Controller {
?.policy; ?.policy;
} }
/// Handles incoming requests and serves the appropriate file.
@override @override
Future<RequestOrResponse> handle(Request request) async { Future<RequestOrResponse> handle(Request request) async {
if (request.method != "GET") { if (request.method != "GET") {
@ -208,6 +226,7 @@ class FileController extends Controller {
..contentType = contentType; ..contentType = contentType;
} }
/// Documents the operations of this controller for API documentation.
@override @override
Map<String, APIOperation> documentOperations( Map<String, APIOperation> documentOperations(
APIDocumentContext context, 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); 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 { 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); _PolicyPair(this.policy, this.shouldApplyToPath);
/// A function that determines if the policy should be applied to a given path.
final bool Function(String) shouldApplyToPath; final bool Function(String) shouldApplyToPath;
/// The cache policy to apply.
final CachePolicy policy; final CachePolicy policy;
} }

View file

@ -1,9 +1,31 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_http/http.dart'; 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 { class HandlerException implements Exception {
/// Constructs a [HandlerException] with the given [Response].
///
/// @param _response The HTTP response associated with this exception.
HandlerException(this._response); 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; Response get response => _response;
/// The private field storing the HTTP response associated with this exception.
final Response _response; final Response _response;
} }

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:protevus_http/http.dart'; 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 /// 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. /// to add mappings in an application's [ApplicationChannel] subclass constructor.
class CodecRegistry { class CodecRegistry {
/// Private constructor to prevent direct instantiation.
CodecRegistry._() { CodecRegistry._() {
add( add(
ContentType("application", "json", charset: "utf-8"), ContentType("application", "json", charset: "utf-8"),
@ -31,10 +41,19 @@ class CodecRegistry {
static CodecRegistry get defaultInstance => _defaultInstance; static CodecRegistry get defaultInstance => _defaultInstance;
static final CodecRegistry _defaultInstance = CodecRegistry._(); static final CodecRegistry _defaultInstance = CodecRegistry._();
/// Map of primary content types to their respective codecs.
final Map<String, Codec> _primaryTypeCodecs = {}; final Map<String, Codec> _primaryTypeCodecs = {};
/// Map of fully specified content types to their respective codecs.
final Map<String, Map<String, Codec>> _fullySpecificedCodecs = {}; final Map<String, Map<String, Codec>> _fullySpecificedCodecs = {};
/// Map of primary content types to their compression settings.
final Map<String, bool> _primaryTypeCompressionMap = {}; final Map<String, bool> _primaryTypeCompressionMap = {};
/// Map of fully specified content types to their compression settings.
final Map<String, Map<String, bool>> _fullySpecifiedCompressionMap = {}; final Map<String, Map<String, bool>> _fullySpecifiedCompressionMap = {};
/// Map of content types to their default charsets.
final Map<String, Map<String, String?>> _defaultCharsetMap = {}; final Map<String, Map<String, String?>> _defaultCharsetMap = {};
/// Adds a custom [codec] for [contentType]. /// Adds a custom [codec] for [contentType].
@ -167,6 +186,7 @@ class CodecRegistry {
return null; return null;
} }
/// Returns a [Codec] for the given [charset].
Codec<String, List<int>> _codecForCharset(String? charset) { Codec<String, List<int>> _codecForCharset(String? charset) {
final encoding = Encoding.getByName(charset); final encoding = Encoding.getByName(charset);
if (encoding == null) { if (encoding == null) {
@ -176,6 +196,7 @@ class CodecRegistry {
return encoding; return encoding;
} }
/// Returns the default charset [Codec] for the given [ContentType].
Codec<String, List<int>>? _defaultCharsetCodecForType(ContentType type) { Codec<String, List<int>>? _defaultCharsetCodecForType(ContentType type) {
final inner = _defaultCharsetMap[type.primaryType]; final inner = _defaultCharsetMap[type.primaryType];
if (inner == null) { 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> { class _FormCodec extends Codec<Map<String, dynamic>?, dynamic> {
const _FormCodec(); const _FormCodec();
@ -201,6 +223,7 @@ class _FormCodec extends Codec<Map<String, dynamic>?, dynamic> {
Converter<String, Map<String, dynamic>> get decoder => const _FormDecoder(); Converter<String, Map<String, dynamic>> get decoder => const _FormDecoder();
} }
/// A [Converter] for encoding form data.
class _FormEncoder extends Converter<Map<String, dynamic>, String> { class _FormEncoder extends Converter<Map<String, dynamic>, String> {
const _FormEncoder(); const _FormEncoder();
@ -209,6 +232,7 @@ class _FormEncoder extends Converter<Map<String, dynamic>, String> {
return data.keys.map((k) => _encodePair(k, data[k])).join("&"); 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 _encodePair(String key, dynamic value) {
String encode(String v) => "$key=${Uri.encodeQueryComponent(v)}"; String encode(String v) => "$key=${Uri.encodeQueryComponent(v)}";
if (value is List<String>) { 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>> { 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, // 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. // 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(); 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 @override
Map<String, dynamic> convert(String data) { Map<String, dynamic> convert(String data) {
return Uri(query: data).queryParametersAll; 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 @override
_FormSink startChunkedConversion(Sink<Map<String, dynamic>> outSink) { _FormSink startChunkedConversion(Sink<Map<String, dynamic>> outSink) {
return _FormSink(outSink); return _FormSink(outSink);
} }
} }
/// A [ChunkedConversionSink] for form data.
class _FormSink implements ChunkedConversionSink<String> { class _FormSink implements ChunkedConversionSink<String> {
_FormSink(this._outSink); _FormSink(this._outSink);
/// The decoder used to convert the form data.
final _FormDecoder decoder = const _FormDecoder(); final _FormDecoder decoder = const _FormDecoder();
/// The output sink for the converted data.
final Sink<Map<String, dynamic>> _outSink; final Sink<Map<String, dynamic>> _outSink;
/// Buffer to accumulate incoming data.
final StringBuffer _buffer = StringBuffer(); 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 @override
void add(String data) { void add(String data) {
_buffer.write(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 @override
void close() { void close() {
_outSink.add(decoder.convert(_buffer.toString())); _outSink.add(decoder.convert(_buffer.toString()));

View file

@ -1,5 +1,13 @@
import 'dart:async'; /*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_database/db.dart'; import 'package:protevus_database/db.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
@ -40,6 +48,8 @@ import 'package:protevus_openapi/v3.dart';
class ManagedObjectController<InstanceType extends ManagedObject> class ManagedObjectController<InstanceType extends ManagedObject>
extends ResourceController { extends ResourceController {
/// Creates an instance of a [ManagedObjectController]. /// Creates an instance of a [ManagedObjectController].
///
/// [context] is the [ManagedContext] used for database operations.
ManagedObjectController(ManagedContext context) : super() { ManagedObjectController(ManagedContext context) : super() {
_query = Query<InstanceType>(context); _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, /// 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. /// 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( ManagedObjectController.forEntity(
ManagedEntity entity, ManagedEntity entity,
ManagedContext context, ManagedContext context,
@ -59,10 +71,13 @@ class ManagedObjectController<InstanceType extends ManagedObject>
/// Returns a route pattern for using [ManagedObjectController]s. /// 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. /// 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) { static String routePattern(String name) {
return "/$name/[:id]"; return "/$name/[:id]";
} }
/// The query used for database operations.
Query<InstanceType>? _query; Query<InstanceType>? _query;
/// Executed prior to a fetch by ID query. /// Executed prior to a fetch by ID query.
@ -92,6 +107,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.notFound(); return Response.notFound();
} }
/// Handles GET requests for a single object by ID.
///
/// [id] is the ID of the object to fetch.
@Operation.get("id") @Operation.get("id")
Future<Response> getObject(@Bind.path("id") String id) async { Future<Response> getObject(@Bind.path("id") String id) async {
final primaryKey = _query!.entity.primaryKey; final primaryKey = _query!.entity.primaryKey;
@ -128,6 +146,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.ok(object); return Response.ok(object);
} }
/// Handles POST requests to create a new object.
@Operation.post() @Operation.post()
Future<Response> createObject() async { Future<Response> createObject() async {
final instance = _query!.entity.instanceOf() as InstanceType; final instance = _query!.entity.instanceOf() as InstanceType;
@ -165,6 +184,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.notFound(); return Response.notFound();
} }
/// Handles DELETE requests to delete an object by ID.
///
/// [id] is the ID of the object to delete.
@Operation.delete("id") @Operation.delete("id")
Future<Response> deleteObject(@Bind.path("id") String id) async { Future<Response> deleteObject(@Bind.path("id") String id) async {
final primaryKey = _query!.entity.primaryKey; final primaryKey = _query!.entity.primaryKey;
@ -208,6 +230,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.notFound(); return Response.notFound();
} }
/// Handles PUT requests to update an object by ID.
///
/// [id] is the ID of the object to update.
@Operation.put("id") @Operation.put("id")
Future<Response> updateObject(@Bind.path("id") String id) async { Future<Response> updateObject(@Bind.path("id") String id) async {
final primaryKey = _query!.entity.primaryKey; final primaryKey = _query!.entity.primaryKey;
@ -247,6 +272,9 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return Response.ok(objects); return Response.ok(objects);
} }
/// Handles GET requests to fetch multiple objects.
///
/// Supports pagination, sorting, and filtering through query parameters.
@Operation.get() @Operation.get()
Future<Response> getObjects({ Future<Response> getObjects({
/// Limits the number of objects returned. /// Limits the number of objects returned.
@ -359,6 +387,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return didFindObjects(results); return didFindObjects(results);
} }
/// Documents the request body for POST and PUT operations.
@override @override
APIRequestBody? documentOperationRequestBody( APIRequestBody? documentOperationRequestBody(
APIDocumentContext context, APIDocumentContext context,
@ -375,6 +404,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return null; return null;
} }
/// Documents the responses for each operation type.
@override @override
Map<String, APIResponse> documentOperationResponses( Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context, APIDocumentContext context,
@ -445,6 +475,7 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return {}; return {};
} }
/// Documents the operations for this controller.
@override @override
Map<String, APIOperation> documentOperations( Map<String, APIOperation> documentOperations(
APIDocumentContext context, APIDocumentContext context,
@ -469,6 +500,10 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return ops; 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( dynamic _getIdentifierFromPath(
String value, String value,
ManagedPropertyDescription? desc, ManagedPropertyDescription? desc,
@ -476,6 +511,11 @@ class ManagedObjectController<InstanceType extends ManagedObject>
return _parseValueForProperty(value, desc, onError: Response.notFound()); 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( dynamic _parseValueForProperty(
String value, String value,
ManagedPropertyDescription? desc, { ManagedPropertyDescription? desc, {

View file

@ -1,5 +1,13 @@
import 'dart:async'; /*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:protevus_database/db.dart'; import 'package:protevus_database/db.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
@ -20,6 +28,8 @@ import 'package:protevus_http/http.dart';
abstract class QueryController<InstanceType extends ManagedObject> abstract class QueryController<InstanceType extends ManagedObject>
extends ResourceController { extends ResourceController {
/// Create an instance of [QueryController]. /// Create an instance of [QueryController].
///
/// [context] is the [ManagedContext] used for database operations.
QueryController(ManagedContext context) : super() { QueryController(ManagedContext context) : super() {
query = Query<InstanceType>(context); 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]. /// 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; 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 @override
FutureOr<RequestOrResponse> willProcessRequest(Request req) { FutureOr<RequestOrResponse> willProcessRequest(Request req) {
if (req.path.orderedVariableNames.isNotEmpty) { if (req.path.orderedVariableNames.isNotEmpty) {
@ -64,6 +80,12 @@ abstract class QueryController<InstanceType extends ManagedObject>
return super.willProcessRequest(req); 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 @override
void didDecodeRequestBody(RequestBody body) { void didDecodeRequestBody(RequestBody body) {
query!.values.readFromMap(body.as()); query!.values.readFromMap(body.as());

View file

@ -1,7 +1,15 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:protevus_auth/auth.dart'; import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
@ -73,6 +81,7 @@ class Request implements RequestOrResponse {
/// null if no permission has been set. /// null if no permission has been set.
Authorization? authorization; Authorization? authorization;
/// List of response modifiers to be applied before sending the response.
List<void Function(Response)>? _responseModifiers; List<void Function(Response)>? _responseModifiers;
/// The acceptable content types for a [Response] returned for this instance. /// The acceptable content types for a [Response] returned for this instance.
@ -125,6 +134,7 @@ class Request implements RequestOrResponse {
return _cachedAcceptableTypes!; return _cachedAcceptableTypes!;
} }
/// Cached list of acceptable content types.
List<ContentType>? _cachedAcceptableTypes; List<ContentType>? _cachedAcceptableTypes;
/// Whether a [Response] may contain a body of type [contentType]. /// Whether a [Response] may contain a body of type [contentType].
@ -205,6 +215,7 @@ class Request implements RequestOrResponse {
_responseModifiers!.add(modifier); _responseModifiers!.add(modifier);
} }
/// Returns a sanitized version of the request headers as a string.
String get _sanitizedHeaders { String get _sanitizedHeaders {
final StringBuffer buf = StringBuffer("{"); final StringBuffer buf = StringBuffer("{");
@ -216,6 +227,7 @@ class Request implements RequestOrResponse {
return buf.toString(); return buf.toString();
} }
/// Truncates a string to a specified length, adding an ellipsis if truncated.
String _truncatedString(String originalString, {int charSize = 128}) { String _truncatedString(String originalString, {int charSize = 128}) {
if (originalString.length <= charSize) { if (originalString.length <= charSize) {
return originalString; return originalString;
@ -302,6 +314,7 @@ class Request implements RequestOrResponse {
throw StateError("Invalid response body. Could not encode."); throw StateError("Invalid response body. Could not encode.");
} }
/// Encodes the response body as bytes, applying compression if necessary.
List<int>? _responseBodyBytes( List<int>? _responseBodyBytes(
Response resp, Response resp,
_Reference<String> compressionType, _Reference<String> compressionType,
@ -346,6 +359,7 @@ class Request implements RequestOrResponse {
return codec.encode(resp.body); return codec.encode(resp.body);
} }
/// Encodes the response body as a stream, applying compression if necessary.
Stream<List<int>> _responseBodyStream( Stream<List<int>> _responseBodyStream(
Response resp, Response resp,
_Reference<String> compressionType, _Reference<String> compressionType,
@ -383,6 +397,7 @@ class Request implements RequestOrResponse {
return codec.encoder.bind(resp.body as Stream); return codec.encoder.bind(resp.body as Stream);
} }
/// Whether the client accepts gzip-encoded response bodies.
bool get _acceptsGzipResponseBody { bool get _acceptsGzipResponseBody {
return raw.headers[HttpHeaders.acceptEncodingHeader] return raw.headers[HttpHeaders.acceptEncodingHeader]
?.any((v) => v.split(",").any((s) => s.trim() == "gzip")) ?? ?.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 { class HTTPStreamingException implements Exception {
/// Creates a new [HTTPStreamingException] with the given underlying exception and stack trace.
HTTPStreamingException(this.underlyingException, this.trace); HTTPStreamingException(this.underlyingException, this.trace);
/// The underlying exception that caused the streaming error.
dynamic underlyingException; dynamic underlyingException;
/// The stack trace associated with the underlying exception.
StackTrace trace; StackTrace trace;
} }
/// A reference wrapper class for holding mutable values.
class _Reference<T> { class _Reference<T> {
/// Creates a new [_Reference] with the given initial value.
_Reference(this.value); _Reference(this.value);
/// The wrapped value.
T? value; T? value;
} }

View file

@ -1,6 +1,14 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Objects that represent a request body, and can be decoded into Dart objects. /// 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. /// See [CodecRegistry] for more information about how data is decoded.
/// ///
/// Decoded data is cached the after it is decoded. /// Decoded data is cached the after it is decoded.
///
/// [request] The HttpRequest object to be decoded.
RequestBody(HttpRequest super.request) RequestBody(HttpRequest super.request)
: _request = request, : _request = request,
_originalByteStream = 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). /// 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; static int maxSize = 1024 * 1024 * 10;
/// The original HttpRequest object.
final HttpRequest _request; 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 => bool get _hasContent =>
_hasContentLength || _request.headers.chunkedTransferEncoding; _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; 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 @override
Stream<List<int>> get bytes { Stream<List<int>> get bytes {
// If content-length is specified, then we can check it for maxSize // If content-length is specified, then we can check it for maxSize
@ -88,18 +111,32 @@ class RequestBody extends BodyDecoder {
return _bufferingController!.stream; return _bufferingController!.stream;
} }
/// Gets the content type of the request.
///
/// Returns null if no content type is specified.
@override @override
ContentType? get contentType => _request.headers.contentType; ContentType? get contentType => _request.headers.contentType;
/// Checks if the request body is empty.
///
/// Returns true if the request has no content.
@override @override
bool get isEmpty => !_hasContent; 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 => bool get isFormData =>
contentType != null && contentType != null &&
contentType!.primaryType == "application" && contentType!.primaryType == "application" &&
contentType!.subType == "x-www-form-urlencoded"; contentType!.subType == "x-www-form-urlencoded";
/// The original byte stream of the request.
final Stream<List<int>> _originalByteStream; final Stream<List<int>> _originalByteStream;
/// A buffering controller for the byte stream when content length is not specified.
StreamController<List<int>>? _bufferingController; StreamController<List<int>>? _bufferingController;
/// The number of bytes read from the request body.
int _bytesRead = 0; int _bytesRead = 0;
} }

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Stores path info for a [Request]. /// Stores path info for a [Request].
@ -10,8 +19,14 @@ class RequestPath {
/// Default constructor for [RequestPath]. /// Default constructor for [RequestPath].
/// ///
/// There is no need to invoke this constructor manually. /// There is no need to invoke this constructor manually.
///
/// [segments] is a list of path segments.
RequestPath(this.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}) { void setSpecification(RouteSpecification spec, {int segmentOffset = 0}) {
final requestIterator = segments.iterator; final requestIterator = segments.iterator;
for (var i = 0; i < segmentOffset; i++) { for (var i = 0; i < segmentOffset; i++) {
@ -50,7 +65,6 @@ class RequestPath {
/// Consider a match specification /users/:id. If the evaluated path is /// Consider a match specification /users/:id. If the evaluated path is
/// /users/2 /// /users/2
/// This property will be {'id' : '2'}. /// This property will be {'id' : '2'}.
///
Map<String, String> variables = {}; Map<String, String> variables = {};
/// A list of the segments in a matched path. /// A list of the segments in a matched path.

View file

@ -1,7 +1,15 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_auth/auth.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. /// 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 abstract class ResourceController extends Controller
implements Recyclable<void> { implements Recyclable<void> {
/// Constructor for ResourceController.
ResourceController() { ResourceController() {
_runtime = _runtime =
(RuntimeContext.current.runtimes[runtimeType] as ControllerRuntime?) (RuntimeContext.current.runtimes[runtimeType] as ControllerRuntime?)
?.resourceController; ?.resourceController;
} }
/// Getter for the recycled state of the controller.
@override @override
void get recycledState => nullptr; void get recycledState => nullptr;
/// The runtime for this ResourceController.
ResourceControllerRuntime? _runtime; ResourceControllerRuntime? _runtime;
/// The request being processed by this [ResourceController]. /// The request being processed by this [ResourceController].
@ -129,11 +140,13 @@ abstract class ResourceController extends Controller
/// this method is not called. /// this method is not called.
void didDecodeRequestBody(RequestBody body) {} void didDecodeRequestBody(RequestBody body) {}
/// Restores the state of the controller.
@override @override
void restore(void state) { void restore(void state) {
/* no op - fetched from static cache in Runtime */ /* no op - fetched from static cache in Runtime */
} }
/// Handles the incoming request.
@override @override
FutureOr<RequestOrResponse> handle(Request request) async { FutureOr<RequestOrResponse> handle(Request request) async {
this.request = request; this.request = request;
@ -226,6 +239,7 @@ abstract class ResourceController extends Controller
return [tag]; return [tag];
} }
/// Documents the operations for this controller.
@override @override
Map<String, APIOperation> documentOperations( Map<String, APIOperation> documentOperations(
APIDocumentContext context, APIDocumentContext context,
@ -235,11 +249,13 @@ abstract class ResourceController extends Controller
return _runtime!.documenter!.documentOperations(this, context, route, path); return _runtime!.documenter!.documentOperations(this, context, route, path);
} }
/// Documents the components for this controller.
@override @override
void documentComponents(APIDocumentContext context) { void documentComponents(APIDocumentContext context) {
_runtime!.documenter?.documentComponents(this, context); _runtime!.documenter?.documentComponents(this, context);
} }
/// Checks if the request content type is supported.
bool _requestContentTypeIsSupported(Request? req) { bool _requestContentTypeIsSupported(Request? req) {
final incomingContentType = request!.raw.headers.contentType; final incomingContentType = request!.raw.headers.contentType;
return acceptedContentTypes.firstWhereOrNull((ct) { return acceptedContentTypes.firstWhereOrNull((ct) {
@ -249,6 +265,7 @@ abstract class ResourceController extends Controller
null; null;
} }
/// Returns a list of allowed HTTP methods for the given path variables.
List<String> _allowedMethodsForPathVariables( List<String> _allowedMethodsForPathVariables(
Iterable<String?> pathVariables, Iterable<String?> pathVariables,
) { ) {
@ -258,6 +275,7 @@ abstract class ResourceController extends Controller
.toList(); .toList();
} }
/// Processes the request and returns a response.
Future<Response> _process() async { Future<Response> _process() async {
if (!request!.body.isEmpty) { if (!request!.body.isEmpty) {
if (!_requestContentTypeIsSupported(request)) { if (!_requestContentTypeIsSupported(request)) {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Binds an instance method in [ResourceController] to an operation. /// Binds an instance method in [ResourceController] to an operation.
@ -14,6 +23,7 @@ import 'package:protevus_http/http.dart';
/// } /// }
/// } /// }
class Operation { class Operation {
/// Creates an [Operation] with the specified [method] and optional path variables.
const Operation( const Operation(
this.method, [ this.method, [
String? pathVariable1, String? pathVariable1,
@ -25,6 +35,7 @@ class Operation {
_pathVariable3 = pathVariable3, _pathVariable3 = pathVariable3,
_pathVariable4 = pathVariable4; _pathVariable4 = pathVariable4;
/// Creates a GET [Operation] with optional path variables.
const Operation.get([ const Operation.get([
String? pathVariable1, String? pathVariable1,
String? pathVariable2, String? pathVariable2,
@ -36,6 +47,7 @@ class Operation {
_pathVariable3 = pathVariable3, _pathVariable3 = pathVariable3,
_pathVariable4 = pathVariable4; _pathVariable4 = pathVariable4;
/// Creates a PUT [Operation] with optional path variables.
const Operation.put([ const Operation.put([
String? pathVariable1, String? pathVariable1,
String? pathVariable2, String? pathVariable2,
@ -47,6 +59,7 @@ class Operation {
_pathVariable3 = pathVariable3, _pathVariable3 = pathVariable3,
_pathVariable4 = pathVariable4; _pathVariable4 = pathVariable4;
/// Creates a POST [Operation] with optional path variables.
const Operation.post([ const Operation.post([
String? pathVariable1, String? pathVariable1,
String? pathVariable2, String? pathVariable2,
@ -58,6 +71,7 @@ class Operation {
_pathVariable3 = pathVariable3, _pathVariable3 = pathVariable3,
_pathVariable4 = pathVariable4; _pathVariable4 = pathVariable4;
/// Creates a DELETE [Operation] with optional path variables.
const Operation.delete([ const Operation.delete([
String? pathVariable1, String? pathVariable1,
String? pathVariable2, String? pathVariable2,
@ -69,10 +83,19 @@ class Operation {
_pathVariable3 = pathVariable3, _pathVariable3 = pathVariable3,
_pathVariable4 = pathVariable4; _pathVariable4 = pathVariable4;
/// The HTTP method for this operation.
final String method; final String method;
/// The first path variable (if any).
final String? _pathVariable1; final String? _pathVariable1;
/// The second path variable (if any).
final String? _pathVariable2; final String? _pathVariable2;
/// The third path variable (if any).
final String? _pathVariable3; final String? _pathVariable3;
/// The fourth path variable (if any).
final String? _pathVariable4; final String? _pathVariable4;
/// Returns a list of all path variables required for this operation. /// Returns a list of all path variables required for this operation.
@ -209,15 +232,26 @@ class Bind {
ignore = null, ignore = null,
reject = null; reject = null;
/// The name of the binding (for query, header, and path bindings).
final String? name; final String? name;
/// The type of binding (query, header, body, or path).
final BindingType bindingType; final BindingType bindingType;
/// List of keys to accept in the request body (for body bindings).
final List<String>? accept; final List<String>? accept;
/// List of keys to ignore in the request body (for body bindings).
final List<String>? ignore; final List<String>? ignore;
/// List of keys to reject in the request body (for body bindings).
final List<String>? reject; final List<String>? reject;
/// List of keys required in the request body (for body bindings).
final List<String>? require; final List<String>? require;
} }
/// Enum representing the types of bindings available.
enum BindingType { query, header, body, path } enum BindingType { query, header, body, path }
/// Marks an [ResourceController] property binding as required. /// Marks an [ResourceController] property binding as required.
@ -245,7 +279,10 @@ enum BindingType { query, header, body, path }
/// } /// }
const RequiredBinding requiredBinding = RequiredBinding(); const RequiredBinding requiredBinding = RequiredBinding();
/// See [requiredBinding]. /// Class representing a required binding.
///
/// See [requiredBinding] for more information.
class RequiredBinding { class RequiredBinding {
/// Creates a [RequiredBinding] instance.
const RequiredBinding(); const RequiredBinding();
} }

View file

@ -1,17 +1,36 @@
import 'dart:async'; /*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_auth/auth.dart'; import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart'; import 'package:protevus_openapi/v3.dart';
/// Abstract class representing the runtime of a ResourceController.
abstract class ResourceControllerRuntime { abstract class ResourceControllerRuntime {
/// List of instance variable parameters.
List<ResourceControllerParameter>? ivarParameters; List<ResourceControllerParameter>? ivarParameters;
/// List of operations supported by the ResourceController.
late List<ResourceControllerOperation> operations; late List<ResourceControllerOperation> operations;
/// Documenter for the ResourceController.
ResourceControllerDocumenter? documenter; 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( ResourceControllerOperation? getOperationRuntime(
String method, String method,
List<String?> pathVariables, 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( void applyRequestProperties(
ResourceController untypedController, ResourceController untypedController,
ResourceControllerOperationInvocationArgs args, ResourceControllerOperationInvocationArgs args,
); );
} }
/// Abstract class for documenting a ResourceController.
abstract class ResourceControllerDocumenter { abstract class ResourceControllerDocumenter {
/// Documents the components of a ResourceController.
///
/// [rc] The ResourceController instance.
/// [context] The API documentation context.
void documentComponents(ResourceController rc, APIDocumentContext 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( List<APIParameter> documentOperationParameters(
ResourceController rc, ResourceController rc,
APIDocumentContext context, APIDocumentContext context,
Operation? operation, 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( APIRequestBody? documentOperationRequestBody(
ResourceController rc, ResourceController rc,
APIDocumentContext context, APIDocumentContext context,
Operation? operation, 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( Map<String, APIOperation> documentOperations(
ResourceController rc, ResourceController rc,
APIDocumentContext context, APIDocumentContext context,
@ -50,7 +100,9 @@ abstract class ResourceControllerDocumenter {
); );
} }
/// Represents an operation in a ResourceController.
class ResourceControllerOperation { class ResourceControllerOperation {
/// Creates a new ResourceControllerOperation.
ResourceControllerOperation({ ResourceControllerOperation({
required this.scopes, required this.scopes,
required this.pathVariables, required this.pathVariables,
@ -61,23 +113,36 @@ class ResourceControllerOperation {
required this.invoker, required this.invoker,
}); });
/// The required authentication scopes for this operation.
final List<AuthScope>? scopes; final List<AuthScope>? scopes;
/// The path variables for this operation.
final List<String> pathVariables; final List<String> pathVariables;
/// The HTTP method for this operation.
final String httpMethod; final String httpMethod;
/// The name of the Dart method implementing this operation.
final String dartMethodName; final String dartMethodName;
/// The positional parameters for this operation.
final List<ResourceControllerParameter> positionalParameters; final List<ResourceControllerParameter> positionalParameters;
/// The named parameters for this operation.
final List<ResourceControllerParameter> namedParameters; final List<ResourceControllerParameter> namedParameters;
/// The function to invoke this operation.
final Future<Response> Function( final Future<Response> Function(
ResourceController resourceController, ResourceController resourceController,
ResourceControllerOperationInvocationArgs args, ResourceControllerOperationInvocationArgs args,
) invoker; ) 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 /// [requestMethod] The HTTP method of the request.
/// path variables are compared. /// [requestPathVariables] The path variables of the request.
///
/// Returns true if the operation is suitable for the request, false otherwise.
bool isSuitableForRequest( bool isSuitableForRequest(
String? requestMethod, String? requestMethod,
List<String?> requestPathVariables, List<String?> requestPathVariables,
@ -94,7 +159,9 @@ class ResourceControllerOperation {
} }
} }
/// Represents a parameter in a ResourceController operation.
class ResourceControllerParameter { class ResourceControllerParameter {
/// Creates a new ResourceControllerParameter.
ResourceControllerParameter({ ResourceControllerParameter({
required this.symbolName, required this.symbolName,
required this.name, required this.name,
@ -109,6 +176,7 @@ class ResourceControllerParameter {
required this.rejectFilter, required this.rejectFilter,
}) : _decoder = decoder; }) : _decoder = decoder;
/// Creates a typed ResourceControllerParameter.
static ResourceControllerParameter make<T>({ static ResourceControllerParameter make<T>({
required String symbolName, required String symbolName,
required String? name, required String? name,
@ -136,22 +204,40 @@ class ResourceControllerParameter {
); );
} }
/// The name of the symbol in the Dart code.
final String symbolName; final String symbolName;
/// The name of the parameter in the API.
final String? name; final String? name;
/// The type of the parameter.
final Type type; final Type type;
/// The default value of the parameter.
final dynamic defaultValue; final dynamic defaultValue;
/// The filter for accepted values.
final List<String>? acceptFilter; final List<String>? acceptFilter;
/// The filter for ignored values.
final List<String>? ignoreFilter; final List<String>? ignoreFilter;
/// The filter for required values.
final List<String>? requireFilter; final List<String>? requireFilter;
/// The filter for rejected values.
final List<String>? rejectFilter; 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; final BindingType location;
/// Indicates if the parameter is required.
final bool isRequired; final bool isRequired;
/// The decoder function for the parameter.
final dynamic Function(dynamic input)? _decoder; final dynamic Function(dynamic input)? _decoder;
/// Gets the API parameter location for this parameter.
APIParameterLocation get apiLocation { APIParameterLocation get apiLocation {
switch (location) { switch (location) {
case BindingType.body: case BindingType.body:
@ -165,6 +251,7 @@ class ResourceControllerParameter {
} }
} }
/// Gets the location name as a string.
String get locationName { String get locationName {
switch (location) { switch (location) {
case BindingType.query: 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) { dynamic decode(Request? request) {
switch (location) { switch (location) {
case BindingType.query: case BindingType.query:
@ -220,8 +312,14 @@ class ResourceControllerParameter {
} }
} }
/// Holds the arguments for invoking a ResourceController operation.
class ResourceControllerOperationInvocationArgs { class ResourceControllerOperationInvocationArgs {
/// The instance variables for the invocation.
late Map<String, dynamic> instanceVariables; late Map<String, dynamic> instanceVariables;
/// The named arguments for the invocation.
late Map<String, dynamic> namedArguments; late Map<String, dynamic> namedArguments;
/// The positional arguments for the invocation.
late List<dynamic> positionalArguments; late List<dynamic> positionalArguments;
} }

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_auth/auth.dart'; import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
@ -33,11 +42,16 @@ import 'package:protevus_http/http.dart';
/// .link(() => Authorizer.bearer(authServer)) /// .link(() => Authorizer.bearer(authServer))
/// .link(() => NoteController()); /// .link(() => NoteController());
class Scope { 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); const Scope(this.scopes);
/// The list of authorization scopes required. /// 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; final List<String> scopes;
} }

View file

@ -1,8 +1,16 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Represents the information in an HTTP response. /// Represents the information in an HTTP response.
@ -14,6 +22,10 @@ class Response implements RequestOrResponse {
/// ///
/// There exist convenience constructors for common response status codes /// There exist convenience constructors for common response status codes
/// and you should prefer to use those. /// 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) { Response(int this.statusCode, Map<String, dynamic>? headers, dynamic body) {
this.body = body; this.body = body;
this.headers = LinkedHashMap<String, dynamic>( this.headers = LinkedHashMap<String, dynamic>(
@ -22,13 +34,18 @@ class Response implements RequestOrResponse {
this.headers.addAll(headers ?? {}); 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}) Response.ok(dynamic body, {Map<String, dynamic>? headers})
: this(HttpStatus.ok, headers, body); : 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( Response.created(
String location, { String location, {
dynamic body, dynamic body,
@ -39,48 +56,73 @@ class Response implements RequestOrResponse {
body, 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}) Response.accepted({Map<String, dynamic>? headers})
: this(HttpStatus.accepted, headers, null); : 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}) Response.noContent({Map<String, dynamic>? headers})
: this(HttpStatus.noContent, headers, null); : 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 /// [lastModified] The last modified date of the resource.
/// and [cachePolicy] is the same policy as applied when this resource was first fetched. /// [cachePolicy] The same policy as applied when this resource was first fetched.
Response.notModified(DateTime lastModified, this.cachePolicy) { Response.notModified(DateTime lastModified, this.cachePolicy) {
statusCode = HttpStatus.notModified; statusCode = HttpStatus.notModified;
headers = {HttpHeaders.lastModifiedHeader: HttpDate.format(lastModified)}; 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}) Response.badRequest({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.badRequest, headers, 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}) Response.unauthorized({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.unauthorized, headers, 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}) Response.forbidden({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.forbidden, headers, 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}) Response.notFound({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.notFound, headers, 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}) Response.conflict({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.conflict, headers, 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}) Response.gone({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.gone, headers, 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}) Response.serverError({Map<String, dynamic>? headers, dynamic body})
: this(HttpStatus.internalServerError, headers, body); : this(HttpStatus.internalServerError, headers, body);
@ -112,6 +154,12 @@ class Response implements RequestOrResponse {
_body = serializedBody ?? initialResponseBody; _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; dynamic _body;
/// Whether or not this instance should buffer its output or send it right away. /// 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. /// See [contentType] for behavior when setting 'content-type' in this property.
Map<String, dynamic> get headers => _headers; 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) { set headers(Map<String, dynamic> h) {
_headers.clear(); _headers.clear();
_headers.addAll(h); _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?>( final Map<String, dynamic> _headers = LinkedHashMap<String, Object?>(
equals: (a, b) => a.toLowerCase() == b.toLowerCase(), equals: (a, b) => a.toLowerCase() == b.toLowerCase(),
hashCode: (key) => key.toLowerCase().hashCode); hashCode: (key) => key.toLowerCase().hashCode);
@ -188,13 +247,14 @@ class Response implements RequestOrResponse {
); );
} }
/// Sets the content type for this response.
set contentType(ContentType? t) { set contentType(ContentType? t) {
_contentType = t; _contentType = t;
} }
ContentType? _contentType; 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. /// This value indicates whether or not [contentType] has been set, or is still using its default value.
bool get hasExplicitlySetContentType => _contentType != null; 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. /// from disk where it is already stored as an encoded list of bytes.
bool encodeBody = true; 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( static Map<String, dynamic> _headersWith(
Map<String, dynamic>? inputHeaders, Map<String, dynamic>? inputHeaders,
Map<String, dynamic> otherHeaders, Map<String, dynamic> otherHeaders,

View file

@ -1,6 +1,19 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Represents a segment of a route path.
class RouteSegment { class RouteSegment {
/// Creates a new RouteSegment from a string segment.
///
/// [segment] The string representation of the route segment.
RouteSegment(String segment) { RouteSegment(String segment) {
if (segment == "*") { if (segment == "*") {
isRemainingMatcher = true; 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({ RouteSegment.direct({
this.literal, this.literal,
this.variableName, this.variableName,
@ -34,18 +53,34 @@ class RouteSegment {
} }
} }
/// The literal string of the segment.
String? literal; String? literal;
/// The name of the variable if this is a variable segment.
String? variableName; String? variableName;
/// The regular expression for matching this segment.
RegExp? matcher; RegExp? matcher;
/// Whether this segment is a literal matcher.
bool get isLiteralMatcher => bool get isLiteralMatcher =>
!isRemainingMatcher && !isVariable && !hasRegularExpression; !isRemainingMatcher && !isVariable && !hasRegularExpression;
/// Whether this segment has a regular expression for matching.
bool get hasRegularExpression => matcher != null; bool get hasRegularExpression => matcher != null;
/// Whether this segment is a variable.
bool get isVariable => variableName != null; bool get isVariable => variableName != null;
/// Whether this segment matches all remaining segments.
bool isRemainingMatcher = false; 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 @override
bool operator ==(Object other) => bool operator ==(Object other) =>
other is RouteSegment && other is RouteSegment &&
@ -54,9 +89,25 @@ class RouteSegment {
isRemainingMatcher == other.isRemainingMatcher && isRemainingMatcher == other.isRemainingMatcher &&
matcher?.pattern == other.matcher?.pattern; 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 @override
int get hashCode => (literal ?? variableName).hashCode; 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 @override
String toString() { String toString() {
if (isLiteralMatcher) { if (isLiteralMatcher) {
@ -75,7 +126,13 @@ class RouteSegment {
} }
} }
/// Represents a node in the route tree.
class RouteNode { 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}) { RouteNode(List<RouteSpecification?> specs, {int depth = 0, RegExp? matcher}) {
patternMatcher = matcher; patternMatcher = matcher;
@ -147,22 +204,35 @@ class RouteNode {
}).toList(); }).toList();
} }
/// Creates a new RouteNode with a specific route specification.
///
/// [specification] The route specification for this node.
RouteNode.withSpecification(this.specification); RouteNode.withSpecification(this.specification);
// Regular expression matcher for this node. May be null. /// Regular expression matcher for this node. May be null.
RegExp? patternMatcher; RegExp? patternMatcher;
/// The controller associated with this route node.
Controller? get controller => specification?.controller; Controller? get controller => specification?.controller;
/// The route specification for this node.
RouteSpecification? specification; RouteSpecification? specification;
// Includes children that are variables with and without regex patterns /// Children nodes that are matched using regular expressions.
List<RouteNode> patternedChildren = []; 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 = {}; Map<String, RouteNode> equalityChildren = {};
// Valid if has child that is a take all (*) segment. /// Child node that matches all remaining segments.
RouteNode? takeAllChild; 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( RouteNode? nodeForPathSegments(
Iterator<String> requestSegments, Iterator<String> requestSegments,
RequestPath path, RequestPath path,
@ -195,6 +265,16 @@ class RouteNode {
return takeAllChild; 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 @override
String toString({int depth = 0}) { String toString({int depth = 0}) {
final buf = StringBuffer(); final buf = StringBuffer();

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
/// Specifies a matchable route path. /// Specifies a matchable route path.
@ -15,6 +24,11 @@ class RouteSpecification {
.toList(); .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( static List<RouteSpecification> specificationsForRoutePattern(
String routePattern, String routePattern,
) { ) {
@ -32,10 +46,16 @@ class RouteSpecification {
/// A reference back to the [Controller] to be used when this specification is matched. /// A reference back to the [Controller] to be used when this specification is matched.
Controller? controller; Controller? controller;
/// Returns a string representation of the route specification.
@override @override
String toString() => segments.join("/"); 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) { List<String> _pathsFromRoutePattern(String inputPattern) {
var routePattern = inputPattern; var routePattern = inputPattern;
var endingOptionalCloseCount = 0; var endingOptionalCloseCount = 0;
@ -102,6 +122,11 @@ List<String> _pathsFromRoutePattern(String inputPattern) {
return patterns; 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) { List<RouteSegment> _splitPathSegments(String inputPath) {
var path = 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. // Once we've gotten into this method, the path has been validated for optionals and regex and optionals have been removed.

View file

@ -1,6 +1,14 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart'; import 'package:protevus_openapi/v3.dart';
@ -18,6 +26,9 @@ import 'package:protevus_openapi/v3.dart';
/// a [Router] is the [ApplicationChannel.entryPoint]. /// a [Router] is the [ApplicationChannel.entryPoint].
class Router extends Controller { class Router extends Controller {
/// Creates a new [Router]. /// 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}) Router({String? basePath, Future Function(Request)? notFoundHandler})
: _unmatchedController = notFoundHandler, : _unmatchedController = notFoundHandler,
_basePathSegments = _basePathSegments =
@ -25,9 +36,16 @@ class Router extends Controller {
policy?.allowCredentials = false; policy?.allowCredentials = false;
} }
/// The root node of the routing tree.
final _RootNode _root = _RootNode(); final _RootNode _root = _RootNode();
/// List of route controllers.
final List<_RouteController> _routeControllers = []; final List<_RouteController> _routeControllers = [];
/// Segments of the base path.
final List<String> _basePathSegments; final List<String> _basePathSegments;
/// Function to handle unmatched requests.
final Function(Request)? _unmatchedController; final Function(Request)? _unmatchedController;
/// A prefix for all routes on this instance. /// A prefix for all routes on this instance.
@ -77,6 +95,7 @@ class Router extends Controller {
return routeController; return routeController;
} }
/// Called when this controller is added to a channel.
@override @override
void didAddToChannel() { void didAddToChannel() {
_root.node = _root.node =
@ -95,6 +114,7 @@ class Router extends Controller {
); );
} }
/// Routers override this method to throw an exception. Use [route] instead.
@override @override
Linkable? linkFunction( Linkable? linkFunction(
FutureOr<RequestOrResponse?> Function(Request request) handle, 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 @override
Future receive(Request req) async { Future receive(Request req) async {
Controller next; Controller next;
@ -142,11 +163,13 @@ class Router extends Controller {
return next.receive(req); return next.receive(req);
} }
/// Router should not handle requests directly.
@override @override
FutureOr<RequestOrResponse> handle(Request request) { FutureOr<RequestOrResponse> handle(Request request) {
throw StateError("Router invoked handle. This is a bug."); throw StateError("Router invoked handle. This is a bug.");
} }
/// Documents the paths for this router.
@override @override
Map<String, APIPath> documentPaths(APIDocumentContext context) { Map<String, APIPath> documentPaths(APIDocumentContext context) {
return _routeControllers.fold(<String, APIPath>{}, (prev, elem) { return _routeControllers.fold(<String, APIPath>{}, (prev, elem) {
@ -155,6 +178,7 @@ class Router extends Controller {
}); });
} }
/// Documents the components for this router.
@override @override
void documentComponents(APIDocumentContext context) { void documentComponents(APIDocumentContext context) {
for (final controller in _routeControllers) { for (final controller in _routeControllers) {
@ -162,11 +186,13 @@ class Router extends Controller {
} }
} }
/// Returns a string representation of this router.
@override @override
String toString() { String toString() {
return _root.node.toString(); return _root.node.toString();
} }
/// Handles unmatched requests.
Future _handleUnhandledRequest(Request req) async { Future _handleUnhandledRequest(Request req) async {
if (_unmatchedController != null) { if (_unmatchedController != null) {
return _unmatchedController(req); return _unmatchedController(req);
@ -184,11 +210,14 @@ class Router extends Controller {
} }
} }
/// Represents the root node of the routing tree.
class _RootNode { class _RootNode {
RouteNode? node; RouteNode? node;
} }
/// Represents a route controller.
class _RouteController extends Controller { class _RouteController extends Controller {
/// Creates a new [_RouteController] with the given specifications.
_RouteController(this.specifications) { _RouteController(this.specifications) {
for (final p in specifications) { for (final p in specifications) {
p.controller = this; p.controller = this;
@ -198,6 +227,7 @@ class _RouteController extends Controller {
/// Route specifications for this controller. /// Route specifications for this controller.
final List<RouteSpecification> specifications; final List<RouteSpecification> specifications;
/// Documents the paths for this route controller.
@override @override
Map<String, APIPath> documentPaths(APIDocumentContext components) { Map<String, APIPath> documentPaths(APIDocumentContext components) {
return specifications.fold(<String, APIPath>{}, (pathMap, spec) { return specifications.fold(<String, APIPath>{}, (pathMap, spec) {
@ -235,6 +265,7 @@ class _RouteController extends Controller {
}); });
} }
/// Handles the request for this route controller.
@override @override
FutureOr<RequestOrResponse> handle(Request request) { FutureOr<RequestOrResponse> handle(Request request) {
return request; return request;

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_http/http.dart'; import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.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 /// 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. /// 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) { APISchemaObject documentSchema(APIDocumentContext context) {
return (RuntimeContext.current[runtimeType] as SerializableRuntime) return (RuntimeContext.current[runtimeType] as SerializableRuntime)
.documentSchema(context); .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 /// 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 /// purposes. [SerializableException]s should be thrown when [object] violates a constraint
/// of the receiver. /// of the receiver.
///
/// [object] The map containing the values to be read.
void readFromMap(Map<String, dynamic> object); void readFromMap(Map<String, dynamic> object);
/// Reads values from [object], after applying filters. /// 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. /// 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). /// 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: /// [object] The map containing the values to be read.
/// /// [accept] If set, only these keys will be accepted from the object.
/// If [accept] is set, all values for the keys that are not given are ignored and discarded. /// [ignore] If set, these keys will be ignored from the object.
/// If [ignore] is set, all values for the given keys are ignored and discarded. /// [reject] If set, the presence of any of these keys will cause an exception.
/// If [reject] is set, if [object] contains any of these keys, a status code 400 exception is thrown. /// [require] If set, all of these keys must be present in the object.
/// 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"]);
void read( void read(
Map<String, dynamic> object, { Map<String, dynamic> object, {
Iterable<String>? accept, 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 /// 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 /// 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. /// each element in the list.
///
/// Returns a [Map<String, dynamic>] representation of the object.
Map<String, dynamic> asMap(); Map<String, dynamic> asMap();
/// Whether a subclass will automatically be registered as a schema component automatically. /// Whether a subclass will automatically be registered as a schema component automatically.
@ -94,11 +104,19 @@ abstract class Serializable {
static bool get shouldAutomaticallyDocument => true; static bool get shouldAutomaticallyDocument => true;
} }
/// Exception thrown when there's an error in serialization or deserialization.
class SerializableException implements HandlerException { class SerializableException implements HandlerException {
/// Constructor for SerializableException.
///
/// [reasons] A list of reasons for the exception.
SerializableException(this.reasons); SerializableException(this.reasons);
/// The reasons for the exception.
final List<String> reasons; final List<String> reasons;
/// Generates a response for this exception.
///
/// Returns a [Response] with a bad request status and error details.
@override @override
Response get response { Response get response {
return Response.badRequest( 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 @override
String toString() { String toString() {
final errorString = response.body["error"] as String?; 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 { 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); APISchemaObject documentSchema(APIDocumentContext context);
} }

View file

@ -7,15 +7,6 @@
* file that was distributed with this source code. * 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. /// This library provides functionality for working with isolates in Dart.
/// It exports three main components: /// It exports three main components:
/// 1. Executable: Defines the structure for tasks that can be executed in isolates. /// 1. Executable: Defines the structure for tasks that can be executed in isolates.

View file

@ -11,25 +11,48 @@ import 'dart:async';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:mirrors'; 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?> { 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"]; 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(); Future<T> execute();
/// The message data passed to the Executable.
final Map<String, dynamic> message; final Map<String, dynamic> message;
/// A SendPort for communicating back to the main isolate.
final SendPort? _sendPort; 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>( U instanceOf<U>(
String typeName, { String typeName, {
List positionalArguments = const [], List positionalArguments = const [],
Map<Symbol, dynamic> namedArguments = const {}, Map<Symbol, dynamic> namedArguments = const {},
Symbol constructorName = Symbol.empty, Symbol constructorName = Symbol.empty,
}) { }) {
// Try to find the ClassMirror in the root library
ClassMirror? typeMirror = currentMirrorSystem() ClassMirror? typeMirror = currentMirrorSystem()
.isolate .isolate
.rootLibrary .rootLibrary
.declarations[Symbol(typeName)] as ClassMirror?; .declarations[Symbol(typeName)] as ClassMirror?;
// If not found in the root library, search in all libraries
typeMirror ??= currentMirrorSystem() typeMirror ??= currentMirrorSystem()
.libraries .libraries
.values .values
@ -44,6 +67,7 @@ abstract class Executable<T extends Object?> {
), ),
) as ClassMirror?; ) as ClassMirror?;
// Create and return a new instance of the specified type
return typeMirror! return typeMirror!
.newInstance( .newInstance(
constructorName, constructorName,
@ -53,10 +77,16 @@ abstract class Executable<T extends Object?> {
.reflectee as U; .reflectee as U;
} }
/// Sends a message back to the main isolate.
///
/// @param message The message to be sent.
void send(dynamic message) { void send(dynamic message) {
_sendPort!.send(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) { void log(String message) {
_sendPort!.send({"_line_": message}); _sendPort!.send({"_line_": message});
} }

View file

@ -12,25 +12,64 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:protevus_isolate/isolate.dart'; 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> { 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( IsolateExecutor(
this.generator, { this.generator, {
this.packageConfigURI, this.packageConfigURI,
this.message = const {}, this.message = const {},
}); });
/// The source generator that provides the code to be executed.
final SourceGenerator generator; final SourceGenerator generator;
/// A map of data to be passed to the isolate.
final Map<String, dynamic> message; final Map<String, dynamic> message;
/// The URI of the package configuration file.
final Uri? packageConfigURI; final Uri? packageConfigURI;
/// A completer that completes when the isolate execution is finished.
final Completer completer = Completer(); 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<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; Stream<String> get console => _logListener.stream;
/// StreamController for managing console output from the isolate.
final StreamController<String> _logListener = StreamController<String>(); final StreamController<String> _logListener = StreamController<String>();
/// StreamController for managing custom events from the isolate.
final StreamController<dynamic> _eventListener = StreamController<dynamic>(); 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 { Future<U> execute() async {
if (packageConfigURI != null && if (packageConfigURI != null &&
!File.fromUri(packageConfigURI!).existsSync()) { !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>( static Future<T> run<T>(
Executable<T> executable, { Executable<T> executable, {
List<String> imports = const [], List<String> imports = const [],

View file

@ -20,7 +20,14 @@ import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:protevus_isolate/isolate.dart'; import 'package:protevus_isolate/isolate.dart';
/// A class responsible for generating source code for isolate execution.
class SourceGenerator { 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( SourceGenerator(
this.executableType, { this.executableType, {
this.imports = const [], this.imports = const [],
@ -28,24 +35,40 @@ class SourceGenerator {
this.additionalContents, this.additionalContents,
}); });
/// The Type of the executable class.
Type executableType; Type executableType;
/// Returns the name of the executable type.
String get typeName => String get typeName =>
MirrorSystem.getName(reflectType(executableType).simpleName); MirrorSystem.getName(reflectType(executableType).simpleName);
/// List of import statements to include in the generated source.
final List<String> imports; final List<String> imports;
/// Optional additional content to append to the generated source.
final String? additionalContents; final String? additionalContents;
/// List of additional Types to include in the generated source.
final List<Type> additionalTypes; 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 { Future<String> get scriptSource async {
final typeSource = (await _getClass(executableType)).toSource(); final typeSource = (await _getClass(executableType)).toSource();
final builder = StringBuffer(); final builder = StringBuffer();
// Add standard imports
builder.writeln("import 'dart:async';"); builder.writeln("import 'dart:async';");
builder.writeln("import 'dart:isolate';"); builder.writeln("import 'dart:isolate';");
builder.writeln("import 'dart:mirrors';"); builder.writeln("import 'dart:mirrors';");
// Add custom imports
for (final anImport in imports) { for (final anImport in imports) {
builder.writeln("import '$anImport';"); builder.writeln("import '$anImport';");
} }
// Add main function for isolate execution
builder.writeln( builder.writeln(
""" """
Future main (List<String> args, Map<String, dynamic> message) async { 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); builder.writeln(typeSource);
// Add Executable base class source
builder.writeln((await _getClass(Executable)).toSource()); builder.writeln((await _getClass(Executable)).toSource());
// Add additional types' sources
for (final type in additionalTypes) { for (final type in additionalTypes) {
final source = await _getClass(type); final source = await _getClass(type);
builder.writeln(source.toSource()); builder.writeln(source.toSource());
} }
// Add additional contents if provided
if (additionalContents != null) { if (additionalContents != null) {
builder.writeln(additionalContents); builder.writeln(additionalContents);
} }
@ -71,6 +100,10 @@ Future main (List<String> args, Map<String, dynamic> message) async {
return builder.toString(); 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 { static Future<ClassDeclaration> _getClass(Type type) async {
final uri = final uri =
await Isolate.resolvePackageUri(reflectClass(type).location!.sourceUri); 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( AnalysisContext _createContext(
String path, { String path, {
ResourceProvider? resourceProvider, ResourceProvider? resourceProvider,

View file

@ -7,10 +7,15 @@
* file that was distributed with this source code. * 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; library runtime;
import 'dart:io'; import 'dart:io';
// Export statements for various components of the runtime package
export 'package:protevus_runtime/src/analyzer.dart'; export 'package:protevus_runtime/src/analyzer.dart';
export 'package:protevus_runtime/src/build.dart'; export 'package:protevus_runtime/src/build.dart';
export 'package:protevus_runtime/src/build_context.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/generator.dart';
export 'package:protevus_runtime/src/mirror_coerce.dart'; export 'package:protevus_runtime/src/mirror_coerce.dart';
export 'package:protevus_runtime/src/mirror_context.dart'; export 'package:protevus_runtime/src/mirror_context.dart';
import 'package:protevus_runtime/runtime.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 /// This compiler is responsible for creating a mirror-free version of the
/// a generated runtime to the replica's pubspec. /// runtime package. It removes dart:mirror dependencies and adds a generated
/// runtime to the package's pubspec.
class RuntimePackageCompiler extends Compiler { 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 @override
Map<String, Object> compile(MirrorContext context) => {}; 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 @override
void deflectPackage(Directory destinationDirectory) { void deflectPackage(Directory destinationDirectory) {
// Rewrite the main library file
final libraryFile = File.fromUri( final libraryFile = File.fromUri(
destinationDirectory.uri.resolve("lib/").resolve("runtime.dart"), 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';", "library runtime;\nexport 'src/context.dart';\nexport 'src/exceptions.dart';",
); );
// Update the context file
final contextFile = File.fromUri( final contextFile = File.fromUri(
destinationDirectory.uri destinationDirectory.uri
.resolve("lib/") .resolve("lib/")
@ -53,6 +76,7 @@ class RuntimePackageCompiler extends Compiler {
); );
contextFile.writeAsStringSync(contextFileContents); contextFile.writeAsStringSync(contextFileContents);
// Modify the pubspec.yaml
final pubspecFile = final pubspecFile =
File.fromUri(destinationDirectory.uri.resolve("pubspec.yaml")); File.fromUri(destinationDirectory.uri.resolve("pubspec.yaml"));
final pubspecContents = pubspecFile.readAsStringSync().replaceFirst( final pubspecContents = pubspecFile.readAsStringSync().replaceFirst(

View file

@ -9,12 +9,30 @@
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
/// Prefix string for List types in type checking.
const String _listPrefix = "List<"; const String _listPrefix = "List<";
/// Prefix string for Map types in type checking.
const String _mapPrefix = "Map<String,"; 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) { T cast<T>(dynamic input) {
try { try {
var typeString = T.toString(); var typeString = T.toString();
// Handle nullable types
if (typeString.endsWith('?')) { if (typeString.endsWith('?')) {
if (input == null) { if (input == null) {
return null as T; return null as T;
@ -22,11 +40,14 @@ T cast<T>(dynamic input) {
typeString = typeString.substring(0, typeString.length - 1); typeString = typeString.substring(0, typeString.length - 1);
} }
} }
// Handle List types
if (typeString.startsWith(_listPrefix)) { if (typeString.startsWith(_listPrefix)) {
if (input is! List) { if (input is! List) {
throw TypeError(); throw TypeError();
} }
// Cast List to various element types
if (typeString.startsWith("List<int>")) { if (typeString.startsWith("List<int>")) {
return List<int>.from(input) as T; return List<int>.from(input) as T;
} else if (typeString.startsWith("List<num>")) { } else if (typeString.startsWith("List<num>")) {
@ -50,12 +71,16 @@ T cast<T>(dynamic input) {
} else if (typeString.startsWith("List<Map<String, dynamic>>")) { } else if (typeString.startsWith("List<Map<String, dynamic>>")) {
return List<Map<String, dynamic>>.from(input) as T; 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) { if (input is! Map) {
throw TypeError(); throw TypeError();
} }
final inputMap = input as Map<String, dynamic>; final inputMap = input as Map<String, dynamic>;
// Cast Map to various value types
if (typeString.startsWith("Map<String, int>")) { if (typeString.startsWith("Map<String, int>")) {
return Map<String, int>.from(inputMap) as T; return Map<String, int>.from(inputMap) as T;
} else if (typeString.startsWith("Map<String, num>")) { } 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; return input as T;
} on TypeError { } on TypeError {
// If a TypeError occurs during casting, throw a TypeCoercionException
throw TypeCoercionException(T, input.runtimeType); throw TypeCoercionException(T, input.runtimeType);
} }
} }

View file

@ -14,7 +14,11 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/physical_file_system.dart'; import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
/// A class for analyzing Dart code.
class CodeAnalyzer { 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) { CodeAnalyzer(this.uri) {
if (!uri.isAbsolute) { if (!uri.isAbsolute) {
throw ArgumentError("'uri' must be absolute for CodeAnalyzer"); throw ArgumentError("'uri' must be absolute for CodeAnalyzer");
@ -27,16 +31,23 @@ class CodeAnalyzer {
} }
} }
/// Gets the path from the URI.
String get path { String get path {
return getPath(uri); return getPath(uri);
} }
/// The URI of the code to analyze.
late final Uri uri; late final Uri uri;
/// The collection of analysis contexts.
late AnalysisContextCollection contexts; late AnalysisContextCollection contexts;
/// A cache of resolved ASTs.
final _resolvedAsts = <String, AnalysisResult>{}; 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 { Future<AnalysisResult> resolveUnitOrLibraryAt(Uri uri) async {
if (FileSystemEntity.isFileSync( if (FileSystemEntity.isFileSync(
uri.toFilePath(windows: Platform.isWindows), 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 { Future<ResolvedLibraryResult> resolveLibraryAt(Uri uri) async {
assert( assert(
FileSystemEntity.isDirectorySync( FileSystemEntity.isDirectorySync(
@ -68,6 +83,10 @@ class CodeAnalyzer {
"${contexts.contexts.map((c) => c.contextRoot.root.toUri()).join(", ")})"); "${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 { Future<ResolvedUnitResult> resolveUnitAt(Uri uri) async {
assert( assert(
FileSystemEntity.isFileSync( FileSystemEntity.isFileSync(
@ -89,6 +108,9 @@ class CodeAnalyzer {
"${contexts.contexts.map((c) => c.contextRoot.root.toUri()).join(", ")})"); "${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) { ClassDeclaration? getClassFromFile(String className, Uri fileUri) {
try { try {
return _getFileAstRoot(fileUri) 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( List<ClassDeclaration> getSubclassesFromFile(
String superclassName, String superclassName,
Uri fileUri, Uri fileUri,
@ -115,6 +140,9 @@ class CodeAnalyzer {
.toList(); .toList();
} }
/// Gets the AST root of the file at the given URI.
///
/// Returns a [CompilationUnit].
CompilationUnit _getFileAstRoot(Uri fileUri) { CompilationUnit _getFileAstRoot(Uri fileUri) {
assert( assert(
FileSystemEntity.isFileSync( FileSystemEntity.isFileSync(
@ -135,6 +163,9 @@ class CodeAnalyzer {
return unit.unit; return unit.unit;
} }
/// Converts the input URI to a normalized path string.
///
/// This is a static utility method.
static String getPath(dynamic inputUri) { static String getPath(dynamic inputUri) {
return PhysicalResourceProvider.INSTANCE.pathContext.normalize( return PhysicalResourceProvider.INSTANCE.pathContext.normalize(
PhysicalResourceProvider.INSTANCE.pathContext.fromUri(inputUri), PhysicalResourceProvider.INSTANCE.pathContext.fromUri(inputUri),

View file

@ -11,16 +11,34 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:mirrors'; import 'dart:mirrors';
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
import 'package:io/io.dart'; import 'package:io/io.dart';
import 'package:package_config/package_config.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 { 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); Build(this.context);
/// The build context for this build operation.
final BuildContext context; 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 { Future execute() async {
final compilers = context.context.compilers; 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 { Future getDependencies() async {
const String cmd = "dart"; 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 { Future compile(Uri srcUri, Uri dstUri) async {
final res = await Process.run( final res = await Process.run(
"dart", "dart",
@ -173,6 +202,13 @@ class Build {
print("${res.stdout}"); 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 { Future copyPackage(Uri srcUri, Uri dstUri) async {
final dstDir = Directory.fromUri(dstUri); final dstDir = Directory.fromUri(dstUri);
if (!dstDir.existsSync()) { 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 { Future<Package> _getPackageInfoForCompiler(Compiler compiler) async {
final compilerUri = reflect(compiler).type.location!.sourceUri; final compilerUri = reflect(compiler).type.location!.sourceUri;

View file

@ -9,7 +9,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:mirrors'; import 'dart:mirrors';
import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/ast.dart';
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
import 'package:package_config/package_config.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:pubspec_parse/pubspec_parse.dart';
import 'package:yaml/yaml.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 { 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( BuildContext(
this.rootLibraryFileUri, this.rootLibraryFileUri,
this.buildDirectoryUri, this.buildDirectoryUri,
@ -30,6 +39,12 @@ class BuildContext {
analyzer = CodeAnalyzer(sourceApplicationDirectory.uri); 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) { factory BuildContext.fromMap(Map<String, dynamic> map) {
return BuildContext( return BuildContext(
Uri.parse(map['rootLibraryFileUri']), 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 => { Map<String, dynamic> get safeMap => {
'rootLibraryFileUri': sourceLibraryFile.uri.toString(), 'rootLibraryFileUri': sourceLibraryFile.uri.toString(),
'buildDirectoryUri': buildDirectoryUri.toString(), 'buildDirectoryUri': buildDirectoryUri.toString(),
@ -50,41 +69,56 @@ class BuildContext {
'forTests': forTests 'forTests': forTests
}; };
/// The CodeAnalyzer instance used for analyzing Dart code.
late final CodeAnalyzer analyzer; 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; final Uri rootLibraryFileUri;
/// A [Uri] to the executable build product file. /// The URI of the executable build product file.
final Uri executableUri; 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; final Uri buildDirectoryUri;
/// The source script for the executable. /// The source script for the executable.
final String source; 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; final bool forTests;
/// Cached PackageConfig instance.
PackageConfig? _packageConfig; PackageConfig? _packageConfig;
/// Optional map of environment variables.
final Map<String, String>? environment; 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; 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 Uri get targetScriptFileUri => forTests
? getDirectory(buildDirectoryUri.resolve("test/")) ? getDirectory(buildDirectoryUri.resolve("test/"))
.uri .uri
.resolve("main_test.dart") .resolve("main_test.dart")
: buildDirectoryUri.resolve("main.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( Pubspec get sourceApplicationPubspec => Pubspec.parse(
File.fromUri(sourceApplicationDirectory.uri.resolve("pubspec.yaml")) File.fromUri(sourceApplicationDirectory.uri.resolve("pubspec.yaml"))
.readAsStringSync(), .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( Map<dynamic, dynamic> get sourceApplicationPubspecMap => loadYaml(
File.fromUri( File.fromUri(
sourceApplicationDirectory.uri.resolve("pubspec.yaml"), sourceApplicationDirectory.uri.resolve("pubspec.yaml"),
@ -101,26 +135,32 @@ class BuildContext {
/// The directory where build artifacts are stored. /// The directory where build artifacts are stored.
Directory get buildDirectory => getDirectory(buildDirectoryUri); Directory get buildDirectory => getDirectory(buildDirectoryUri);
/// The generated runtime directory /// The generated runtime directory.
Directory get buildRuntimeDirectory => Directory get buildRuntimeDirectory =>
getDirectory(buildDirectoryUri.resolve("generated_runtime/")); getDirectory(buildDirectoryUri.resolve("generated_runtime/"));
/// Directory for compiled packages /// Directory for compiled packages.
Directory get buildPackagesDirectory => Directory get buildPackagesDirectory =>
getDirectory(buildDirectoryUri.resolve("packages/")); getDirectory(buildDirectoryUri.resolve("packages/"));
/// Directory for compiled application /// Directory for the compiled application.
Directory get buildApplicationDirectory => getDirectory( Directory get buildApplicationDirectory => getDirectory(
buildPackagesDirectory.uri.resolve("${sourceApplicationPubspec.name}/"), 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 { Future<PackageConfig> get packageConfig async {
return _packageConfig ??= return _packageConfig ??=
(await findPackageConfig(sourceApplicationDirectory))!; (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) { Directory getDirectory(Uri uri) {
final dir = Directory.fromUri(uri); final dir = Directory.fromUri(uri);
if (!dir.existsSync()) { if (!dir.existsSync()) {
@ -129,7 +169,10 @@ class BuildContext {
return dir; 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) { File getFile(Uri uri) {
final file = File.fromUri(uri); final file = File.fromUri(uri);
if (!file.parent.existsSync()) { if (!file.parent.existsSync()) {
@ -139,6 +182,11 @@ class BuildContext {
return file; 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 { Future<Package?> getPackageFromUri(Uri? uri) async {
if (uri == null) { if (uri == null) {
return null; return null;
@ -152,6 +200,13 @@ class BuildContext {
return null; 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({ Future<List<String>> getImportDirectives({
Uri? uri, Uri? uri,
String? source, String? source,
@ -201,6 +256,10 @@ class BuildContext {
return imports; 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 { Future<ClassDeclaration?> getClassDeclarationFromType(Type type) async {
final classMirror = reflectType(type); final classMirror = reflectType(type);
Uri uri = classMirror.location!.sourceUri; 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) { Future<FieldDeclaration?> _getField(ClassMirror type, String propertyName) {
return getClassDeclarationFromType(type.reflectedType).then((cd) { return getClassDeclarationFromType(type.reflectedType).then((cd) {
try { 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( Future<List<Annotation>> getAnnotationsFromField(
Type type1, Type type1,
String propertyName, String propertyName,

View file

@ -8,20 +8,31 @@
*/ */
import 'dart:io'; import 'dart:io';
import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/ast.dart';
import 'package:protevus_isolate/isolate.dart'; import 'package:protevus_isolate/isolate.dart';
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
import 'package:io/io.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 { 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) { BuildExecutable(Map<String, dynamic> message) : super(message) {
context = BuildContext.fromMap(message); context = BuildContext.fromMap(message);
} }
/// The build context for this executable.
late final BuildContext context; 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 @override
Future execute() async { Future execute() async {
final build = Build(context); 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 { 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); BuildManager(this.context);
/// The build context for this manager.
final BuildContext context; final BuildContext context;
/// Gets the URI of the source directory.
Uri get sourceDirectoryUri => context.sourceApplicationDirectory.uri; 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 { Future build() async {
if (!context.buildDirectory.existsSync()) { if (!context.buildDirectory.existsSync()) {
context.buildDirectory.createSync(); context.buildDirectory.createSync();
} }
// Here is where we need to provide a temporary copy of the script file with the main function stripped; // Create 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
var scriptSource = context.source; var scriptSource = context.source;
final strippedScriptFile = File.fromUri(context.targetScriptFileUri) final strippedScriptFile = File.fromUri(context.targetScriptFileUri)
..writeAsStringSync(scriptSource); ..writeAsStringSync(scriptSource);
@ -52,6 +75,7 @@ class BuildManager {
final parsedUnit = analyzerContext.currentSession final parsedUnit = analyzerContext.currentSession
.getParsedUnit(analyzer.path) as ParsedUnitResult; .getParsedUnit(analyzer.path) as ParsedUnitResult;
// Find and remove all main functions
final mainFunctions = parsedUnit.unit.declarations final mainFunctions = parsedUnit.unit.declarations
.whereType<FunctionDeclaration>() .whereType<FunctionDeclaration>()
.where((f) => f.name.value() == "main") .where((f) => f.name.value() == "main")
@ -63,12 +87,14 @@ class BuildManager {
strippedScriptFile.writeAsStringSync(scriptSource); strippedScriptFile.writeAsStringSync(scriptSource);
// Copy the 'not_tests' directory if it exists
try { try {
await copyPath( await copyPath(
context.sourceApplicationDirectory.uri.resolve('test/not_tests').path, context.sourceApplicationDirectory.uri.resolve('test/not_tests').path,
context.buildDirectoryUri.resolve('not_tests').path); context.buildDirectoryUri.resolve('not_tests').path);
} catch (_) {} } catch (_) {}
// Run the build executable in an isolate
await IsolateExecutor.run( await IsolateExecutor.run(
BuildExecutable(context.safeMap), BuildExecutable(context.safeMap),
packageConfigURI: 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 { Future clean() async {
if (context.buildDirectory.existsSync()) { if (context.buildDirectory.existsSync()) {
context.buildDirectory.deleteSync(recursive: true); context.buildDirectory.deleteSync(recursive: true);

View file

@ -8,32 +8,70 @@
*/ */
import 'dart:io'; import 'dart:io';
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
/// An abstract class that defines the interface for compilers in the Protevus Platform.
///
/// 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 { abstract class Compiler {
/// Modifies a package on the filesystem in order to remove dart:mirrors from the package. /// Modifies a package on the filesystem to remove dart:mirrors usage.
/// ///
/// A copy of this compiler's package will be written to [destinationDirectory]. /// This method creates a copy of the compiler's package in the [destinationDirectory]
/// This method is overridden to modify the contents of that directory /// and modifies its contents to remove all uses of dart:mirrors. This is crucial for
/// to remove all uses of dart:mirrors. /// preparing packages for environments where reflection is not available or desired.
/// ///
/// Packages should export their [Compiler] in their main library file and only /// Packages should export their [Compiler] in their main library file and only
/// import mirrors in files directly or transitively imported by the Compiler file. /// 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. /// 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); 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); 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) {} 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) => []; 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 { 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 { Future<String> compile(BuildContext ctx) async {
throw UnimplementedError(); throw UnimplementedError();
} }

View file

@ -10,20 +10,26 @@
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
/// Contextual values used during runtime. /// 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 { abstract class RuntimeContext {
/// The current [RuntimeContext] available to the executing application. /// 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. /// depending on the execution type.
static final RuntimeContext current = instance; static final RuntimeContext current = instance;
/// The runtimes available to the executing application. /// 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; 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] /// Callers typically invoke this operator, passing their [runtimeType]
/// in order to retrieve their runtime object. /// to retrieve their runtime object.
/// ///
/// It is important to note that a runtime object must exist for every /// 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. /// 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`, /// 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` /// `Subclass` must also have a runtime. The runtime objects for both `Subclass` and `Base`
/// must be the same type. /// 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]; 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); 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 { 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); RuntimeCollection(this.map);
/// The underlying map storing runtime objects.
final Map<String, Object> map; final Map<String, Object> map;
/// Returns an iterable of all runtime objects in the collection.
Iterable<Object> get iterable => map.values; 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) { Object operator [](Type t) {
//todo: optimize by keeping a cache where keys are of type [Type] to avoid the //todo: optimize by keeping a cache where keys are of type [Type] to avoid the
// expensive indexOf and substring calls in this method // 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 /// Use this annotation on a type to exclude it from compilation.
/// compilation.
class PreventCompilation { class PreventCompilation {
/// Creates a constant instance of [PreventCompilation].
const PreventCompilation(); const PreventCompilation();
} }

View file

@ -7,13 +7,38 @@
* file that was distributed with this source code. * 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 { 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); TypeCoercionException(this.expectedType, this.actualType);
/// The type that was expected for the conversion.
final Type expectedType; final Type expectedType;
/// The actual type of the value that couldn't be converted.
final Type actualType; final Type actualType;
@override @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() { String toString() {
return "input is not expected type '$expectedType' (input is '$actualType')"; return "input is not expected type '$expectedType' (input is '$actualType')";
} }

View file

@ -10,16 +10,32 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
/// Token used to replace directives in the loader shell.
const String _directiveToken = "___DIRECTIVES___"; const String _directiveToken = "___DIRECTIVES___";
/// Token used to replace assignments in the loader shell.
const String _assignmentToken = "___ASSIGNMENTS___"; 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 { class RuntimeGenerator {
/// List of runtime elements to be generated.
final _elements = <_RuntimeElement>[]; 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}) { void addRuntime({required String name, required String source}) {
_elements.add(_RuntimeElement(name, 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 { Future<void> writeTo(Uri directoryUri) async {
final dir = Directory.fromUri(directoryUri); final dir = Directory.fromUri(directoryUri);
final libDir = Directory.fromUri(dir.uri.resolve("lib/")); 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 => """ String get pubspecSource => """
name: generated_runtime name: generated_runtime
description: A runtime generated by package:conduit_runtime description: A runtime generated by package:conduit_runtime
@ -57,6 +74,9 @@ environment:
sdk: '>=3.4.0 <4.0.0' 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 => """ String get _loaderShell => """
import 'package:conduit_runtime/runtime.dart'; import 'package:conduit_runtime/runtime.dart';
import 'package:conduit_runtime/slow_coerce.dart' as runtime_cast; 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 { String get loaderSource {
return _loaderShell return _loaderShell
.replaceFirst(_directiveToken, _directives) .replaceFirst(_directiveToken, _directives)
.replaceFirst(_assignmentToken, _assignments); .replaceFirst(_assignmentToken, _assignments);
} }
/// Generates the import directives for all runtime elements.
String get _directives { String get _directives {
final buf = StringBuffer(); final buf = StringBuffer();
@ -98,6 +123,7 @@ class GeneratedContext extends RuntimeContext {
return buf.toString(); return buf.toString();
} }
/// Generates the assignments for all runtime elements.
String get _assignments { String get _assignments {
final buf = StringBuffer(); final buf = StringBuffer();
@ -109,14 +135,24 @@ class GeneratedContext extends RuntimeContext {
} }
} }
/// A class representing a single runtime element.
class _RuntimeElement { 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); _RuntimeElement(this.typeName, this.source);
/// The name of the type for this runtime element.
final String typeName; final String typeName;
/// The source code of this runtime element.
final String source; final String source;
/// Returns the relative URI for this runtime element's file.
Uri get relativeUri => Uri.file("${typeName.toLowerCase()}.dart"); Uri get relativeUri => Uri.file("${typeName.toLowerCase()}.dart");
/// Returns the import alias for this runtime element.
String get importAlias { String get importAlias {
return "g_${typeName.toLowerCase()}"; return "g_${typeName.toLowerCase()}";
} }

View file

@ -8,9 +8,23 @@
*/ */
import 'dart:mirrors'; import 'dart:mirrors';
import 'package:protevus_runtime/runtime.dart'; 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) { Object runtimeCast(Object object, TypeMirror intoType) {
final exceptionToThrow = final exceptionToThrow =
TypeCoercionException(intoType.reflectedType, object.runtimeType); TypeCoercionException(intoType.reflectedType, object.runtimeType);
@ -51,6 +65,16 @@ Object runtimeCast(Object object, TypeMirror intoType) {
throw exceptionToThrow; 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) { bool isTypeFullyPrimitive(TypeMirror type) {
if (type == reflectType(dynamic)) { if (type == reflectType(dynamic)) {
return true; return true;

View file

@ -8,12 +8,19 @@
*/ */
import 'dart:mirrors'; import 'dart:mirrors';
import 'package:protevus_runtime/runtime.dart'; import 'package:protevus_runtime/runtime.dart';
/// Global instance of the MirrorContext.
RuntimeContext instance = 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 { class MirrorContext extends RuntimeContext {
/// Private constructor to ensure singleton instance.
///
/// Initializes the context by compiling all available runtimes.
MirrorContext._() { MirrorContext._() {
final m = <String, Object>{}; final m = <String, Object>{};
@ -31,6 +38,9 @@ class MirrorContext extends RuntimeContext {
runtimes = RuntimeCollection(m); runtimes = RuntimeCollection(m);
} }
/// List of all class mirrors in the current mirror system.
///
/// Excludes classes marked with @PreventCompilation.
final List<ClassMirror> types = currentMirrorSystem() final List<ClassMirror> types = currentMirrorSystem()
.libraries .libraries
.values .values
@ -40,6 +50,9 @@ class MirrorContext extends RuntimeContext {
.where((cm) => firstMetadataOfType<PreventCompilation>(cm) == null) .where((cm) => firstMetadataOfType<PreventCompilation>(cm) == null)
.toList(); .toList();
/// List of all available compilers.
///
/// Returns instances of non-abstract classes that are subclasses of Compiler.
List<Compiler> get compilers { List<Compiler> get compilers {
return types return types
.where((b) => b.isSubclassOf(reflectClass(Compiler)) && !b.isAbstract) .where((b) => b.isSubclassOf(reflectClass(Compiler)) && !b.isAbstract)
@ -47,6 +60,10 @@ class MirrorContext extends RuntimeContext {
.toList(); .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) { List<ClassMirror> getSubclassesOf(Type type) {
final mirror = reflectClass(type); final mirror = reflectClass(type);
return types.where((decl) { return types.where((decl) {
@ -68,6 +85,13 @@ class MirrorContext extends RuntimeContext {
}).toList(); }).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 @override
T coerce<T>(dynamic input) { T coerce<T>(dynamic input) {
try { 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}) { T? firstMetadataOfType<T>(DeclarationMirror dm, {TypeMirror? dynamicType}) {
final tMirror = dynamicType ?? reflectType(T); final tMirror = dynamicType ?? reflectType(T);
try { try {