From 02a1be3c0e8d97d5de3f639a30916b124a691707 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Fri, 4 Jul 2025 17:41:26 +0000 Subject: [PATCH 01/10] remove pharaoh next, too much work --- packages/pharaoh/lib/pharaoh_next.dart | 5 - .../pharaoh/lib/src/_next/_core/config.dart | 68 ---- .../lib/src/_next/_core/container.dart | 18 -- .../lib/src/_next/_core/core_impl.dart | 47 --- .../lib/src/_next/_core/reflector.dart | 154 ---------- .../lib/src/_next/_router/definition.dart | 211 ------------- .../pharaoh/lib/src/_next/_router/meta.dart | 157 ---------- .../pharaoh/lib/src/_next/_router/utils.dart | 12 - .../lib/src/_next/_validation/dto.dart | 67 ---- .../lib/src/_next/_validation/meta.dart | 28 -- packages/pharaoh/lib/src/_next/core.dart | 204 ------------ packages/pharaoh/lib/src/_next/http.dart | 70 ----- packages/pharaoh/lib/src/_next/router.dart | 108 ------- .../pharaoh/lib/src/_next/validation.dart | 85 ----- .../test/pharaoh_next/config/config_test.dart | 36 --- .../core/application_factory_test.dart | 53 ---- .../test/pharaoh_next/core/core_test.dart | 111 ------- .../test/pharaoh_next/http/meta_test.dart | 290 ------------------ .../test/pharaoh_next/router_test.dart | 228 -------------- .../validation/validation_test.dart | 214 ------------- 20 files changed, 2166 deletions(-) delete mode 100644 packages/pharaoh/lib/pharaoh_next.dart delete mode 100644 packages/pharaoh/lib/src/_next/_core/config.dart delete mode 100644 packages/pharaoh/lib/src/_next/_core/container.dart delete mode 100644 packages/pharaoh/lib/src/_next/_core/core_impl.dart delete mode 100644 packages/pharaoh/lib/src/_next/_core/reflector.dart delete mode 100644 packages/pharaoh/lib/src/_next/_router/definition.dart delete mode 100644 packages/pharaoh/lib/src/_next/_router/meta.dart delete mode 100644 packages/pharaoh/lib/src/_next/_router/utils.dart delete mode 100644 packages/pharaoh/lib/src/_next/_validation/dto.dart delete mode 100644 packages/pharaoh/lib/src/_next/_validation/meta.dart delete mode 100644 packages/pharaoh/lib/src/_next/core.dart delete mode 100644 packages/pharaoh/lib/src/_next/http.dart delete mode 100644 packages/pharaoh/lib/src/_next/router.dart delete mode 100644 packages/pharaoh/lib/src/_next/validation.dart delete mode 100644 packages/pharaoh/test/pharaoh_next/config/config_test.dart delete mode 100644 packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart delete mode 100644 packages/pharaoh/test/pharaoh_next/core/core_test.dart delete mode 100644 packages/pharaoh/test/pharaoh_next/http/meta_test.dart delete mode 100644 packages/pharaoh/test/pharaoh_next/router_test.dart delete mode 100644 packages/pharaoh/test/pharaoh_next/validation/validation_test.dart diff --git a/packages/pharaoh/lib/pharaoh_next.dart b/packages/pharaoh/lib/pharaoh_next.dart deleted file mode 100644 index 4f8c8bd0..00000000 --- a/packages/pharaoh/lib/pharaoh_next.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'pharaoh.dart'; -export 'src/_next/core.dart'; -export 'src/_next/http.dart'; -export 'src/_next/router.dart'; -export 'src/_next/validation.dart'; diff --git a/packages/pharaoh/lib/src/_next/_core/config.dart b/packages/pharaoh/lib/src/_next/_core/config.dart deleted file mode 100644 index 5769aaf9..00000000 --- a/packages/pharaoh/lib/src/_next/_core/config.dart +++ /dev/null @@ -1,68 +0,0 @@ -part of '../core.dart'; - -DotEnv? _env; - -T env(String name, T defaultValue) { - _env ??= DotEnv(quiet: true, includePlatformEnvironment: true)..load(); - final strVal = _env![name]; - if (strVal == null) return defaultValue; - - final parsedVal = switch (T) { - const (String) => strVal, - const (int) => int.parse(strVal), - const (num) => num.parse(strVal), - const (bool) => bool.parse(strVal), - const (double) => double.parse(strVal), - const (List) => jsonDecode(strVal), - _ => throw ArgumentError.value( - T, null, 'Unsupported Type used in `env` call.'), - }; - return parsedVal as T; -} - -extension ConfigExtension on Map { - T getValue(String name, {T? defaultValue, bool allowEmpty = false}) { - final value = this[name] ?? defaultValue; - if (value is! T) { - throw ArgumentError.value( - value, null, 'Invalid value provided for $name'); - } - if (value != null && value.toString().trim().isEmpty && !allowEmpty) { - throw ArgumentError.value( - value, null, 'Empty value not allowed for $name'); - } - return value; - } -} - -class AppConfig { - final String name; - final String environment; - final bool isDebug; - final String timezone; - final String locale; - final String key; - final int? _port; - final String? _url; - - Uri get _uri { - final uri = Uri.parse(_url!); - return _port == null ? uri : uri.replace(port: _port); - } - - int get port => _uri.port; - - String get url => _uri.toString(); - - const AppConfig({ - required this.name, - required this.environment, - required this.isDebug, - required this.key, - this.timezone = 'UTC', - this.locale = 'en', - int? port, - String? url, - }) : _port = port, - _url = url; -} diff --git a/packages/pharaoh/lib/src/_next/_core/container.dart b/packages/pharaoh/lib/src/_next/_core/container.dart deleted file mode 100644 index 5b2a115e..00000000 --- a/packages/pharaoh/lib/src/_next/_core/container.dart +++ /dev/null @@ -1,18 +0,0 @@ -part of '../core.dart'; - -final GetIt _getIt = GetIt.instance; - -T instanceFromRegistry({Type? type}) { - type ??= T; - try { - return _getIt.get(type: type) as T; - } catch (_) { - throw Exception('Dependency not found in registry: $type'); - } -} - -T registerSingleton(T instance) { - return _getIt.registerSingleton(instance); -} - -bool isRegistered() => _getIt.isRegistered(); diff --git a/packages/pharaoh/lib/src/_next/_core/core_impl.dart b/packages/pharaoh/lib/src/_next/_core/core_impl.dart deleted file mode 100644 index b6ac958c..00000000 --- a/packages/pharaoh/lib/src/_next/_core/core_impl.dart +++ /dev/null @@ -1,47 +0,0 @@ -// ignore_for_file: avoid_function_literals_in_foreach_calls - -part of '../core.dart'; - -class _PharaohNextImpl implements Application { - late final AppConfig _appConfig; - late final Spanner _spanner; - - ViewEngine? _viewEngine; - - _PharaohNextImpl(this._appConfig, this._spanner); - - @override - T singleton(T instance) => registerSingleton(instance); - - @override - T instanceOf() => instanceFromRegistry(); - - @override - void useRoutes(RoutesResolver routeResolver) { - final routes = routeResolver.call(); - routes.forEach((route) => route.commit(_spanner)); - } - - @override - void useViewEngine(ViewEngine viewEngine) => _viewEngine = viewEngine; - - @override - AppConfig get config => _appConfig; - - @override - String get name => config.name; - - @override - String get url => config.url; - - @override - int get port => config.port; - - Pharaoh _createPharaohInstance({OnErrorCallback? onException}) { - final pharaoh = Pharaoh()..useSpanner(_spanner); - Pharaoh.viewEngine = _viewEngine; - - if (onException != null) pharaoh.onError(onException); - return pharaoh; - } -} diff --git a/packages/pharaoh/lib/src/_next/_core/reflector.dart b/packages/pharaoh/lib/src/_next/_core/reflector.dart deleted file mode 100644 index 3148d0e0..00000000 --- a/packages/pharaoh/lib/src/_next/_core/reflector.dart +++ /dev/null @@ -1,154 +0,0 @@ -part of '../core.dart'; - -class Injectable extends r.Reflectable { - const Injectable() - : super( - r.invokingCapability, - r.metadataCapability, - r.newInstanceCapability, - r.declarationsCapability, - r.reflectedTypeCapability, - r.typeRelationsCapability, - const r.InstanceInvokeCapability('^[^_]'), - r.subtypeQuantifyCapability, - ); -} - -const unnamedConstructor = ''; - -const inject = Injectable(); - -List filteredDeclarationsOf( - r.ClassMirror cm, predicate) { - var result = []; - cm.declarations.forEach((k, v) { - if (predicate(v)) result.add(v as X); - }); - return result; -} - -r.ClassMirror reflectType(Type type) { - try { - return inject.reflectType(type) as r.ClassMirror; - } catch (e) { - throw UnsupportedError( - 'Unable to reflect on $type. Re-run your build command'); - } -} - -extension ClassMirrorExtensions on r.ClassMirror { - List get variables { - return filteredDeclarationsOf(this, (v) => v is r.VariableMirror); - } - - List get getters { - return filteredDeclarationsOf( - this, (v) => v is r.MethodMirror && v.isGetter); - } - - List get setters { - return filteredDeclarationsOf( - this, (v) => v is r.MethodMirror && v.isSetter); - } - - List get methods { - return filteredDeclarationsOf( - this, (v) => v is r.MethodMirror && v.isRegularMethod); - } -} - -T createNewInstance(Type classType) { - final classMirror = reflectType(classType); - final constructorMethod = classMirror.declarations.entries - .firstWhereOrNull((e) => e.key == '$classType') - ?.value as r.MethodMirror?; - final constructorParameters = constructorMethod?.parameters ?? []; - if (constructorParameters.isEmpty) { - return classMirror.newInstance(unnamedConstructor, const []) as T; - } - - final namedDeps = constructorParameters - .where((e) => e.isNamed) - .map((e) => ( - name: e.simpleName, - instance: instanceFromRegistry(type: e.reflectedType) - )) - .fold>( - {}, (prev, e) => prev..[Symbol(e.name)] = e.instance); - - final dependencies = constructorParameters - .where((e) => !e.isNamed) - .map((e) => instanceFromRegistry(type: e.reflectedType)) - .toList(); - - return classMirror.newInstance(unnamedConstructor, dependencies, namedDeps) - as T; -} - -ControllerMethod parseControllerMethod(ControllerMethodDefinition defn) { - final type = defn.$1; - final method = defn.$2; - - final ctrlMirror = inject.reflectType(type) as r.ClassMirror; - if (ctrlMirror.superclass?.reflectedType != HTTPController) { - throw ArgumentError('$type must extend BaseController'); - } - - final methods = ctrlMirror.instanceMembers.values.whereType(); - final actualMethod = - methods.firstWhereOrNull((e) => e.simpleName == symbolToString(method)); - if (actualMethod == null) { - throw ArgumentError( - '$type does not have method #${symbolToString(method)}'); - } - - final parameters = actualMethod.parameters; - if (parameters.isEmpty) return ControllerMethod(defn); - - if (parameters.any((e) => e.metadata.length > 1)) { - throw ArgumentError( - 'Multiple annotations using on $type #${symbolToString(method)} parameter'); - } - - final params = parameters.map((e) { - final meta = e.metadata.first; - if (meta is! RequestAnnotation) { - throw ArgumentError( - 'Invalid annotation $meta used on $type #${symbolToString(method)} parameter', - ); - } - - final paramType = e.reflectedType; - final maybeDto = _tryResolveDtoInstance(paramType); - - return ControllerMethodParam( - e.simpleName, - paramType, - defaultValue: e.defaultValue, - optional: e.isOptional, - meta: meta, - dto: maybeDto, - ); - }); - - return ControllerMethod(defn, params); -} - -BaseDTO? _tryResolveDtoInstance(Type type) { - try { - final mirror = dtoReflector.reflectType(type) as r.ClassMirror; - return mirror.newInstance(unnamedConstructor, []) as BaseDTO; - } on r.NoSuchCapabilityError catch (_) { - return null; - } -} - -void ensureIsSubTypeOf(Type objectType) { - try { - final type = reflectType(objectType); - if (type.superclass!.reflectedType != Parent) throw Exception(); - } catch (e) { - throw ArgumentError.value(objectType, 'Invalid Type provided', - 'Ensure your class extends `$Parent` class'); - } -} diff --git a/packages/pharaoh/lib/src/_next/_router/definition.dart b/packages/pharaoh/lib/src/_next/_router/definition.dart deleted file mode 100644 index 01a4e7f5..00000000 --- a/packages/pharaoh/lib/src/_next/_router/definition.dart +++ /dev/null @@ -1,211 +0,0 @@ -part of '../router.dart'; - -enum RouteDefinitionType { route, group, middleware } - -class RouteMapping { - final List methods; - final String _path; - - @visibleForTesting - String get stringVal => '${methods.map((e) => e.name).toList()}: $_path'; - - String get path => _path; - - const RouteMapping(this.methods, this._path); - - RouteMapping prefix(String prefix) { - final newPath = prefix == BASE_PATH - ? _path - : _path == BASE_PATH - ? prefix - : '$prefix$_path'; - return RouteMapping(methods, newPath); - } -} - -abstract class RouteDefinition { - late RouteMapping route; - final RouteDefinitionType type; - - RouteDefinition(this.type); - - void commit(Spanner spanner); - - RouteDefinition _prefix(String prefix) => this..route = route.prefix(prefix); -} - -class UseAliasedMiddleware { - final String alias; - - UseAliasedMiddleware(this.alias); - - Iterable get mdw => - ApplicationFactory.resolveMiddlewareForGroup(alias); - - RouteGroupDefinition group( - String name, - List routes, { - String? prefix, - }) { - return RouteGroupDefinition._(name, prefix: prefix, definitions: routes) - ..middleware(mdw); - } - - RouteGroupDefinition routes(List routes) { - return RouteGroupDefinition._( - BASE_PATH, - definitions: routes, - )..middleware(mdw); - } -} - -class _MiddlewareDefinition extends RouteDefinition { - final Middleware mdw; - - _MiddlewareDefinition(this.mdw, RouteMapping route) - : super(RouteDefinitionType.middleware) { - this.route = route; - } - - @override - void commit(Spanner spanner) => spanner.addMiddleware(route.path, mdw); -} - -typedef ControllerMethodDefinition = (Type controller, Symbol symbol); - -class ControllerMethod { - final ControllerMethodDefinition method; - final Iterable params; - - String get methodName => symbolToString(method.$2); - - Type get controller => method.$1; - - ControllerMethod(this.method, [this.params = const []]); -} - -class ControllerMethodParam { - final String name; - final Type type; - final bool optional; - final dynamic defaultValue; - final RequestAnnotation? meta; - - final BaseDTO? dto; - - const ControllerMethodParam( - this.name, - this.type, { - this.meta, - this.optional = false, - this.defaultValue, - this.dto, - }); -} - -class ControllerRouteMethodDefinition extends RouteDefinition { - final ControllerMethod method; - - ControllerRouteMethodDefinition( - ControllerMethodDefinition defn, - RouteMapping mapping, - ) : method = parseControllerMethod(defn), - super(RouteDefinitionType.route) { - route = mapping; - } - - @override - void commit(Spanner spanner) { - final handler = ApplicationFactory.buildControllerMethod(method); - for (final routeMethod in route.methods) { - spanner.addRoute(routeMethod, route.path, useRequestHandler(handler)); - } - } -} - -class RouteGroupDefinition extends RouteDefinition { - final String name; - final List defns = []; - - List get paths => defns.map((e) => e.route.stringVal).toList(); - - RouteGroupDefinition._( - this.name, { - String? prefix, - Iterable definitions = const [], - }) : super(RouteDefinitionType.group) { - final r = (prefix ?? name).toLowerCase(); - final routePath = r.startsWith(BASE_PATH) ? r : '/$r'; - route = RouteMapping([HTTPMethod.ALL], routePath); - if (definitions.isEmpty) { - throw StateError('Route definitions not provided for group'); - } - _unwrapRoutes(definitions); - } - - void _unwrapRoutes(Iterable routes) { - for (final subRoute in routes) { - if (subRoute is! RouteGroupDefinition) { - defns.add(subRoute._prefix(route.path)); - continue; - } - - for (var e in subRoute.defns) { - defns.add(e._prefix(route.path)); - } - } - } - - void middleware(Iterable func) { - if (func.isEmpty) return; - final mdwDefn = - _MiddlewareDefinition(func.reduce((val, e) => val.chain(e)), route); - defns.insert(0, mdwDefn); - } - - @override - void commit(Spanner spanner) { - for (final mdw in defns) { - mdw.commit(spanner); - } - } -} - -typedef RequestHandlerWithApp = Function( - Application app, - Request req, - Response res, -); - -class FunctionalRouteDefinition extends RouteDefinition { - final HTTPMethod method; - final String path; - - final Middleware? _middleware; - final Middleware? _requestHandler; - - FunctionalRouteDefinition.route( - this.method, this.path, RequestHandler handler) - : _middleware = null, - _requestHandler = useRequestHandler(handler), - super(RouteDefinitionType.route) { - route = RouteMapping([method], path); - } - - FunctionalRouteDefinition.middleware(this.path, Middleware handler) - : _requestHandler = null, - _middleware = handler, - method = HTTPMethod.ALL, - super(RouteDefinitionType.middleware) { - route = RouteMapping([method], path); - } - - @override - void commit(Spanner spanner) { - if (_middleware != null) { - spanner.addMiddleware(path, _middleware!); - } else if (_requestHandler != null) { - spanner.addRoute(method, path, _requestHandler!); - } - } -} diff --git a/packages/pharaoh/lib/src/_next/_router/meta.dart b/packages/pharaoh/lib/src/_next/_router/meta.dart deleted file mode 100644 index 43c334ea..00000000 --- a/packages/pharaoh/lib/src/_next/_router/meta.dart +++ /dev/null @@ -1,157 +0,0 @@ -part of '../router.dart'; - -abstract class RequestAnnotation { - final String? name; - - const RequestAnnotation([this.name]); - - T process(Request request, ControllerMethodParam methodParam); -} - -enum ValidationErrorLocation { param, query, body, header } - -class RequestValidationError extends Error { - final String message; - final Map? errors; - final ValidationErrorLocation location; - - RequestValidationError.param(this.message) - : location = ValidationErrorLocation.param, - errors = null; - - RequestValidationError.header(this.message) - : location = ValidationErrorLocation.header, - errors = null; - - RequestValidationError.query(this.message) - : location = ValidationErrorLocation.query, - errors = null; - - RequestValidationError.body(this.message) - : location = ValidationErrorLocation.body, - errors = null; - - RequestValidationError.errors(this.location, this.errors) : message = ''; - - Map get errorBody => { - 'location': location.name, - if (errors != null) - 'errors': errors!.entries.map((e) => '${e.key}: ${e.value}').toList(), - if (message.isNotEmpty) 'errors': [message], - }; - - @override - String toString() => errorBody.toString(); -} - -/// Use this to annotate a parameter in a controller method -/// which will be resolved to the request body. -/// -/// Example: create(@Body() user) {} -class Body extends RequestAnnotation { - const Body(); - - @override - process(Request request, ControllerMethodParam methodParam) { - final body = request.body; - if (body == null) { - if (methodParam.optional) return null; - throw RequestValidationError.body( - EzValidator.globalLocale.required('body')); - } - - final dtoInstance = methodParam.dto; - if (dtoInstance != null) return dtoInstance..make(request); - - final type = methodParam.type; - if (type != dynamic && body.runtimeType != type) { - throw RequestValidationError.body( - EzValidator.globalLocale.isTypeOf('${methodParam.type}', 'body')); - } - - return body; - } -} - -/// Use this to annotate a parameter in a controller method -/// which will be resolved to a parameter in the request path. -/// -/// `/users//details` Example: getUser(@Param() String userId) {} -class Param extends RequestAnnotation { - const Param([super.name]); - - @override - process(Request request, ControllerMethodParam methodParam) { - final paramName = name ?? methodParam.name; - final value = request.params[paramName] ?? methodParam.defaultValue; - final parsedValue = _parseValue(value, methodParam.type); - if (parsedValue == null) { - throw RequestValidationError.param( - EzValidator.globalLocale.isTypeOf('${methodParam.type}', paramName)); - } - return parsedValue; - } -} - -/// Use this to annotate a parameter in a controller method -/// which will be resolved to a parameter in the request query params. -/// -/// `/users?name=Chima` Example: searchUsers(@Query() String name) {} -class Query extends RequestAnnotation { - const Query([super.name]); - - @override - process(Request request, ControllerMethodParam methodParam) { - final paramName = name ?? methodParam.name; - final value = request.query[paramName] ?? methodParam.defaultValue; - if (!methodParam.optional && value == null) { - throw RequestValidationError.query( - EzValidator.globalLocale.required(paramName)); - } - - final parsedValue = _parseValue(value, methodParam.type); - if (parsedValue == null) { - throw RequestValidationError.query( - EzValidator.globalLocale.isTypeOf('${methodParam.type}', paramName)); - } - return parsedValue; - } -} - -class Header extends RequestAnnotation { - const Header([super.name]); - - @override - process(Request request, ControllerMethodParam methodParam) { - final paramName = name ?? methodParam.name; - final value = request.headers[paramName] ?? methodParam.defaultValue; - if (!methodParam.optional && value == null) { - throw RequestValidationError.header( - EzValidator.globalLocale.required(paramName)); - } - - final parsedValue = _parseValue(value, methodParam.type); - if (parsedValue == null) { - throw RequestValidationError.header( - EzValidator.globalLocale.isTypeOf('${methodParam.type}', paramName)); - } - return parsedValue; - } -} - -_parseValue(dynamic value, Type type) { - if (value.runtimeType == type) return value; - value = value.toString(); - return switch (type) { - const (int) => int.tryParse(value), - const (double) => double.tryParse(value), - const (bool) => value == 'true', - const (List) || const (Map) => jsonDecode(value), - _ => value, - }; -} - -const param = Param(); -const query = Query(); -const body = Body(); -const header = Header(); diff --git a/packages/pharaoh/lib/src/_next/_router/utils.dart b/packages/pharaoh/lib/src/_next/_router/utils.dart deleted file mode 100644 index 0f3fc96b..00000000 --- a/packages/pharaoh/lib/src/_next/_router/utils.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of '../router.dart'; - -String cleanRoute(String route) { - final result = - route.replaceAll(RegExp(r'/+'), '/').replaceAll(RegExp(r'/$'), ''); - return result.isEmpty ? '/' : result; -} - -String symbolToString(Symbol symbol) { - final str = symbol.toString(); - return str.substring(8, str.length - 2); -} diff --git a/packages/pharaoh/lib/src/_next/_validation/dto.dart b/packages/pharaoh/lib/src/_next/_validation/dto.dart deleted file mode 100644 index 5b3f1b16..00000000 --- a/packages/pharaoh/lib/src/_next/_validation/dto.dart +++ /dev/null @@ -1,67 +0,0 @@ -part of '../validation.dart'; - -const _instanceInvoke = r.InstanceInvokeCapability('^[^_]'); - -class DtoReflector extends r.Reflectable { - const DtoReflector() - : super( - r.typeCapability, - r.metadataCapability, - r.newInstanceCapability, - r.declarationsCapability, - r.reflectedTypeCapability, - _instanceInvoke, - r.subtypeQuantifyCapability); -} - -@protected -const dtoReflector = DtoReflector(); - -abstract interface class _BaseDTOImpl { - late Map data; - - void make(Request request) { - data = const {}; - final (result, errors) = schema.validateSync(request.body ?? {}); - if (errors.isNotEmpty) { - throw RequestValidationError.errors(ValidationErrorLocation.body, errors); - } - data = Map.from(result); - } - - EzSchema? _schemaCache; - - EzSchema get schema { - if (_schemaCache != null) return _schemaCache!; - - final mirror = dtoReflector.reflectType(runtimeType) as r.ClassMirror; - final properties = mirror.getters.where((e) => e.isAbstract); - - final entries = properties.map((prop) { - final returnType = prop.reflectedReturnType; - final meta = - prop.metadata.whereType().firstOrNull ?? - ezRequired(returnType); - - if (meta.propertyType != returnType) { - throw ArgumentError( - 'Type Mismatch between ${meta.runtimeType}(${meta.propertyType}) & $runtimeType class property ${prop.simpleName}->($returnType)'); - } - - return MapEntry(meta.name ?? prop.simpleName, meta.validator); - }); - - final entriesToMap = entries.fold>>( - {}, (prev, curr) => prev..[curr.key] = curr.value); - return _schemaCache = EzSchema.shape(entriesToMap); - } -} - -@dtoReflector -abstract class BaseDTO extends _BaseDTOImpl { - @override - noSuchMethod(Invocation invocation) { - final property = symbolToString(invocation.memberName); - return data[property]; - } -} diff --git a/packages/pharaoh/lib/src/_next/_validation/meta.dart b/packages/pharaoh/lib/src/_next/_validation/meta.dart deleted file mode 100644 index 055a7752..00000000 --- a/packages/pharaoh/lib/src/_next/_validation/meta.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of '../validation.dart'; - -abstract class ClassPropertyValidator { - final String? name; - - /// TODO: we need to be able to infer nullability also from the type - /// we'll need reflection for that, tho currently, the reason i'm not - /// doing it is because of the amount of code the library (reflectable) - /// generates just to enable this capability - final bool optional; - - final T? defaultVal; - - Type get propertyType => T; - - const ClassPropertyValidator({ - this.name, - this.defaultVal, - this.optional = false, - }); - - EzValidator get validator { - final base = EzValidator(defaultValue: defaultVal, optional: optional); - return optional - ? base.isType(propertyType) - : base.required().isType(propertyType); - } -} diff --git a/packages/pharaoh/lib/src/_next/core.dart b/packages/pharaoh/lib/src/_next/core.dart deleted file mode 100644 index c857ae3a..00000000 --- a/packages/pharaoh/lib/src/_next/core.dart +++ /dev/null @@ -1,204 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:pharaoh/pharaoh.dart'; -import 'package:reflectable/reflectable.dart' as r; -import 'package:spanner/spanner.dart'; -import 'package:spookie/spookie.dart' as spookie; -import 'package:collection/collection.dart'; -import 'package:dotenv/dotenv.dart'; -import 'package:get_it/get_it.dart'; -import 'package:meta/meta.dart'; - -import 'http.dart'; -import 'router.dart'; -import 'validation.dart'; - -part '_core/core_impl.dart'; -part '_core/config.dart'; -part '_core/container.dart'; -part '_core/reflector.dart'; - -typedef RoutesResolver = List Function(); - -mixin class AppInstance { - Application get app => Application._instance; - - AppConfig get config => app.config; -} - -/// Use this to override the application exceptiosn handler -typedef ApplicationExceptionsHandler = FutureOr Function( - Object exception, - ReqRes reqRes, -); - -abstract interface class Application { - Application(AppConfig config); - - static late final Application _instance; - - String get name; - - String get url; - - int get port; - - AppConfig get config; - - T singleton(T instance); - - T instanceOf(); - - void useRoutes(RoutesResolver routeResolver); - - void useViewEngine(ViewEngine viewEngine); -} - -abstract class ApplicationFactory { - final AppConfig appConfig; - - List get providers; - - /// The application's global HTTP middleware stack. - /// - /// These middleware are run during every request to your application. - /// Types here must extends [Middleware]. - List get middlewares; - - /// The application's route middleware groups. - /// - /// Types here must extends [Middleware]. - final Map> middlewareGroups = {}; - - static Map> _middlewareGroups = {}; - - Middleware? _globalMdwCache; - @nonVirtual - Middleware? get globalMiddleware { - if (_globalMdwCache != null) return _globalMdwCache!; - if (middlewares.isEmpty) return null; - return _globalMdwCache = - middlewares.map(_buildHandlerFunc).reduce((val, e) => val.chain(e)); - } - - ApplicationFactory(this.appConfig) { - providers.forEach(ensureIsSubTypeOf); - middlewares.forEach(ensureIsSubTypeOf); - for (final types in middlewareGroups.values) { - types.map(ensureIsSubTypeOf); - } - _middlewareGroups = middlewareGroups; - } - - Future bootstrap({bool listen = true}) async { - await _bootstrapComponents(appConfig); - - if (listen) await startServer(); - } - - Future startServer() async { - final app = Application._instance as _PharaohNextImpl; - - await app - ._createPharaohInstance(onException: onApplicationException) - .listen(port: app.port); - } - - Future _bootstrapComponents(AppConfig config) async { - final spanner = Spanner()..addMiddleware('/', bodyParser); - Application._instance = _PharaohNextImpl(config, spanner); - - final providerInstances = providers.map(createNewInstance); - - /// register dependencies - for (final instance in providerInstances) { - await Future.sync(instance.register); - } - - if (globalMiddleware != null) { - spanner.addMiddleware('/', globalMiddleware!); - } - - /// boot providers - for (final provider in providerInstances) { - await Future.sync(provider.boot); - } - } - - static RequestHandler buildControllerMethod(ControllerMethod method) { - final params = method.params; - - return (req, res) { - final methodName = method.methodName; - final instance = createNewInstance(method.controller); - final mirror = inject.reflect(instance); - - mirror - ..invokeSetter('request', req) - ..invokeSetter('response', res); - - late Function() methodCall; - - if (params.isNotEmpty) { - final args = _resolveControllerMethodArgs(req, method); - methodCall = () => mirror.invoke(methodName, args); - } else { - methodCall = () => mirror.invoke(methodName, []); - } - - return Future.sync(methodCall); - }; - } - - static List _resolveControllerMethodArgs( - Request request, ControllerMethod method) { - if (method.params.isEmpty) return []; - - final args = []; - - for (final param in method.params) { - final meta = param.meta; - if (meta != null) { - args.add(meta.process(request, param)); - continue; - } - } - return args; - } - - static Iterable resolveMiddlewareForGroup(String group) { - final middlewareGroup = ApplicationFactory._middlewareGroups[group]; - if (middlewareGroup == null) { - throw ArgumentError('Middleware group `$group` does not exist'); - } - return middlewareGroup.map(_buildHandlerFunc); - } - - static Middleware _buildHandlerFunc(Type type) { - final instance = createNewInstance(type); - return instance.handler ?? instance.handle; - } - - @visibleForTesting - Future get tester { - final app = (Application._instance as _PharaohNextImpl); - return spookie.request( - app._createPharaohInstance(onException: onApplicationException)); - } - - FutureOr onApplicationException( - PharaohError error, - Request request, - Response response, - ) async { - final exception = error.exception; - if (exception is RequestValidationError) { - return response.json(exception, statusCode: HttpStatus.badRequest); - } - return response.internalServerError(exception.toString()); - } -} diff --git a/packages/pharaoh/lib/src/_next/http.dart b/packages/pharaoh/lib/src/_next/http.dart deleted file mode 100644 index 0c60583b..00000000 --- a/packages/pharaoh/lib/src/_next/http.dart +++ /dev/null @@ -1,70 +0,0 @@ -library; - -import 'dart:io'; - -import '../http/request.dart'; -import '../http/response.dart'; -import '../middleware/session_mw.dart'; -import '../http/router.dart'; -import 'core.dart'; - -@inject -abstract class ClassMiddleware with AppInstance { - handle(Request req, Response res, NextFunction next) { - next(); - } - - Middleware? get handler => null; -} - -@inject -abstract class ServiceProvider with AppInstance { - static List get defaultProviders => []; - - void boot() {} - - void register() {} -} - -@inject -abstract class HTTPController with AppInstance { - late final Request request; - - late final Response response; - - Map get params => request.params; - - Map get queryParams => request.query; - - Map get headers => request.headers; - - Session? get session => request.session; - - get requestBody => request.body; - - bool get expectsJson { - final headerValue = - request.headers[HttpHeaders.acceptEncodingHeader]?.toString(); - return headerValue != null && headerValue.contains('application/json'); - } - - Response badRequest([String? message]) { - const status = 422; - if (message == null) return response.status(status); - return response.json({'error': message}, statusCode: status); - } - - Response notFound([String? message]) { - const status = 404; - if (message == null) return response.status(status); - return response.json({'error': message}, statusCode: status); - } - - Response jsonResponse(data, {int statusCode = 200}) { - return response.json(data, statusCode: statusCode); - } - - Response redirectTo(String url, {int statusCode = 302}) { - return response.redirect(url, statusCode); - } -} diff --git a/packages/pharaoh/lib/src/_next/router.dart b/packages/pharaoh/lib/src/_next/router.dart deleted file mode 100644 index 33ab43a2..00000000 --- a/packages/pharaoh/lib/src/_next/router.dart +++ /dev/null @@ -1,108 +0,0 @@ -library router; - -import 'dart:convert'; - -import 'package:spanner/spanner.dart'; -import 'package:spanner/src/tree/tree.dart' show BASE_PATH; -import 'package:ez_validator_dart/ez_validator.dart'; -import 'package:grammer/grammer.dart'; -import 'package:meta/meta.dart'; -import '../http/request.dart'; -import '../http/response.dart'; -import '../http/router.dart'; -import 'validation.dart'; -import 'core.dart'; - -part '_router/definition.dart'; -part '_router/meta.dart'; -part '_router/utils.dart'; - -abstract interface class Route { - static UseAliasedMiddleware middleware(String name) => - UseAliasedMiddleware(name); - - static ControllerRouteMethodDefinition get( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.GET], path)); - - static ControllerRouteMethodDefinition head( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.HEAD], path)); - - static ControllerRouteMethodDefinition post( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.POST], path)); - - static ControllerRouteMethodDefinition put( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.PUT], path)); - - static ControllerRouteMethodDefinition delete( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.DELETE], path)); - - static ControllerRouteMethodDefinition patch( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.PATCH], path)); - - static ControllerRouteMethodDefinition options( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.OPTIONS], path)); - - static ControllerRouteMethodDefinition trace( - String path, ControllerMethodDefinition defn) => - ControllerRouteMethodDefinition( - defn, RouteMapping([HTTPMethod.TRACE], path)); - - static ControllerRouteMethodDefinition mapping( - List methods, - String path, - ControllerMethodDefinition defn, - ) { - var mapping = RouteMapping(methods, path); - if (methods.contains(HTTPMethod.ALL)) { - mapping = RouteMapping([HTTPMethod.ALL], path); - } - return ControllerRouteMethodDefinition(defn, mapping); - } - - static RouteGroupDefinition group(String name, List routes, - {String? prefix}) => - RouteGroupDefinition._(name, definitions: routes, prefix: prefix); - - static RouteGroupDefinition resource(String resource, Type controller, - {String? parameterName}) { - resource = resource.toLowerCase(); - - final resourceId = - '${(parameterName ?? resource).toSingular().toLowerCase()}Id'; - - return Route.group(resource, [ - Route.get('/', (controller, #index)), - Route.get('/<$resourceId>', (controller, #show)), - Route.post('/', (controller, #create)), - Route.put('/<$resourceId>', (controller, #update)), - Route.patch('/<$resourceId>', (controller, #update)), - Route.delete('/<$resourceId>', (controller, #delete)) - ]); - } - - static FunctionalRouteDefinition route( - HTTPMethod method, String path, RequestHandler handler) => - FunctionalRouteDefinition.route(method, path, handler); - - static FunctionalRouteDefinition notFound(RequestHandler handler, - [HTTPMethod method = HTTPMethod.ALL]) => - Route.route(method, '/*', handler); -} - -Middleware useAliasedMiddleware(String alias) => - ApplicationFactory.resolveMiddlewareForGroup(alias) - .reduce((val, e) => val.chain(e)); diff --git a/packages/pharaoh/lib/src/_next/validation.dart b/packages/pharaoh/lib/src/_next/validation.dart deleted file mode 100644 index 5ad84950..00000000 --- a/packages/pharaoh/lib/src/_next/validation.dart +++ /dev/null @@ -1,85 +0,0 @@ -// ignore_for_file: camel_case_types - -import 'package:collection/collection.dart'; -import 'package:ez_validator_dart/ez_validator.dart'; -import 'package:meta/meta.dart'; -import 'package:pharaoh/src/_next/core.dart'; -import 'package:reflectable/reflectable.dart' as r; - -import '../http/request.dart'; -import 'router.dart'; - -part '_validation/dto.dart'; -part '_validation/meta.dart'; - -class ezEmail extends ClassPropertyValidator { - final String? message; - - const ezEmail({super.name, super.defaultVal, super.optional, this.message}); - - @override - EzValidator get validator => super.validator.email(message); -} - -class ezDateTime extends ClassPropertyValidator { - final String? message; - - final DateTime? minDate, maxDate; - - const ezDateTime( - {super.name, - super.defaultVal, - super.optional, - this.message, - this.maxDate, - this.minDate}); - - @override - EzValidator get validator { - final base = super.validator.date(message); - if (minDate != null) return base.minDate(minDate!); - if (maxDate != null) return base.maxDate(maxDate!); - return base; - } -} - -class ezMinLength extends ClassPropertyValidator { - final int value; - - const ezMinLength(this.value); - - @override - EzValidator get validator => super.validator.minLength(value); -} - -class ezMaxLength extends ClassPropertyValidator { - final int value; - - const ezMaxLength(this.value); - - @override - EzValidator get validator => super.validator.maxLength(value); -} - -class ezRequired extends ClassPropertyValidator { - final Type? type; - - const ezRequired([this.type]); - - @override - Type get propertyType => type ?? T; -} - -class ezOptional extends ClassPropertyValidator { - final Type type; - final Object? defaultValue; - - const ezOptional(this.type, {this.defaultValue}) - : super(defaultVal: defaultValue); - - @override - Type get propertyType => type; - - @override - bool get optional => true; -} diff --git a/packages/pharaoh/test/pharaoh_next/config/config_test.dart b/packages/pharaoh/test/pharaoh_next/config/config_test.dart deleted file mode 100644 index cfa594cf..00000000 --- a/packages/pharaoh/test/pharaoh_next/config/config_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:pharaoh/pharaoh_next.dart'; -import 'package:spookie/spookie.dart'; - -import '../core/core_test.dart'; -import './config_test.reflectable.dart' as r; - -Matcher throwsArgumentErrorWithMessage(String message) => - throwsA(isA().having((p0) => p0.message, '', message)); - -class AppServiceProvider extends ServiceProvider {} - -void main() { - setUpAll(() => r.initializeReflectable()); - - group('App Config Test', () { - test('should return AppConfig instance', () async { - final testApp = TestKidsApp( - middlewares: [TestMiddleware], providers: [AppServiceProvider]); - expect(testApp, isNotNull); - }); - - test('should use prioritize `port` over port in `url`', () { - const config = AppConfig( - name: 'Foo Bar', - environment: 'debug', - isDebug: true, - key: 'asdfajkl', - url: 'http://localhost:3000', - port: 4000, - ); - - expect(config.url, 'http://localhost:4000'); - expect(config.port, 4000); - }); - }); -} diff --git a/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart b/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart deleted file mode 100644 index 0ca92747..00000000 --- a/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:pharaoh/pharaoh_next.dart'; -import 'package:spookie/spookie.dart'; - -import 'application_factory_test.reflectable.dart'; - -class TestHttpController extends HTTPController { - Future index() async { - return response.ok('Hello World'); - } - - Future show(@query int userId) async { - return response.ok('User $userId'); - } -} - -void main() { - initializeReflectable(); - - group('ApplicationFactory', () { - group('.buildControllerMethod', () { - group('should return request handler', () { - test('for method with no args', () async { - final indexMethod = ControllerMethod((TestHttpController, #index)); - final handler = ApplicationFactory.buildControllerMethod(indexMethod); - - expect(handler, isA()); - - await (await request(Pharaoh()..get('/', handler))) - .get('/') - .expectStatus(200) - .expectBody('Hello World') - .test(); - }); - - test('for method with args', () async { - final showMethod = ControllerMethod( - (TestHttpController, #show), - [ControllerMethodParam('userId', int, meta: query)], - ); - - final handler = ApplicationFactory.buildControllerMethod(showMethod); - expect(handler, isA()); - - await (await request(Pharaoh()..get('/test', handler))) - .get('/test?userId=2345') - .expectStatus(200) - .expectBody('User 2345') - .test(); - }); - }); - }); - }); -} diff --git a/packages/pharaoh/test/pharaoh_next/core/core_test.dart b/packages/pharaoh/test/pharaoh_next/core/core_test.dart deleted file mode 100644 index 58a49fc5..00000000 --- a/packages/pharaoh/test/pharaoh_next/core/core_test.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:pharaoh/pharaoh_next.dart'; -import 'package:spookie/spookie.dart'; - -import '../config/config_test.dart'; -import 'core_test.reflectable.dart'; - -const appConfig = AppConfig( - name: 'Test App', - environment: 'production', - isDebug: false, - url: 'http://localhost', - port: 3000, - key: 'askdfjal;ksdjkajl;j', -); - -class TestMiddleware extends ClassMiddleware {} - -class FoobarMiddleware extends ClassMiddleware { - @override - Middleware get handler => (req, res, next) => next(); -} - -class TestKidsApp extends ApplicationFactory { - final AppConfig? config; - - TestKidsApp({ - this.providers = const [], - this.middlewares = const [], - this.config, - }) : super(config ?? appConfig); - - @override - final List providers; - - @override - final List middlewares; - - @override - Map> get middlewareGroups => { - 'api': [FoobarMiddleware], - 'web': [String] - }; -} - -void main() { - initializeReflectable(); - - group('Core', () { - final testApp = TestKidsApp(middlewares: [TestMiddleware]); - - group('should error', () { - test('when invalid provider type passed', () { - expect( - () => - TestKidsApp(middlewares: [TestMiddleware], providers: [String]), - throwsArgumentErrorWithMessage( - 'Ensure your class extends `ServiceProvider` class')); - }); - - test('when invalid middleware type passed middlewares is not valid', () { - expect( - () => TestKidsApp( - middlewares: [String], providers: [AppServiceProvider]), - throwsArgumentErrorWithMessage( - 'Ensure your class extends `ClassMiddleware` class')); - }); - }); - - test('should resolve global middleware', () { - expect(testApp.globalMiddleware, isA()); - }); - - group('when middleware group', () { - test('should resolve', () { - final group = Route.middleware('api').group('Users', [ - Route.route(HTTPMethod.GET, '/', (req, res) => null), - ]); - - expect(group.paths, ['[ALL]: /users', '[GET]: /users']); - }); - - test('should error when not exist', () { - expect( - () => Route.middleware('foo').group('Users', [ - Route.route(HTTPMethod.GET, '/', (req, res) => null), - ]), - throwsA( - isA().having((p0) => p0.message, 'message', - 'Middleware group `foo` does not exist'), - ), - ); - }); - }); - - test('should throw if type is not subtype of Middleware', () { - final middlewares = ApplicationFactory.resolveMiddlewareForGroup('api'); - expect(middlewares, isA>()); - - expect(middlewares.length, 1); - - expect(() => ApplicationFactory.resolveMiddlewareForGroup('web'), - throwsA(isA())); - }); - - test('should return tester', () async { - await testApp.bootstrap(listen: false); - - expect(await testApp.tester, isA()); - }); - }); -} diff --git a/packages/pharaoh/test/pharaoh_next/http/meta_test.dart b/packages/pharaoh/test/pharaoh_next/http/meta_test.dart deleted file mode 100644 index cbb93c4e..00000000 --- a/packages/pharaoh/test/pharaoh_next/http/meta_test.dart +++ /dev/null @@ -1,290 +0,0 @@ -import 'dart:io'; - -import 'package:pharaoh/pharaoh_next.dart'; - -import 'package:spookie/spookie.dart'; - -import 'meta_test.reflectable.dart'; - -class TestDTO extends BaseDTO { - String get username; - - String get lastname; - - int get age; -} - -Pharaoh get pharaohWithErrorHdler => Pharaoh() - ..onError((error, req, res) { - final exception = error.exception; - if (exception is RequestValidationError) { - return res.json(exception.errorBody, statusCode: 422); - } - - return res.internalServerError(error.toString()); - }); - -void main() { - initializeReflectable(); - - group('Meta', () { - group('Param', () { - test('should use name set in meta', () async { - final app = pharaohWithErrorHdler - ..get('//hello', (req, res) { - final actualParam = Param('userId'); - const ctrlMethodParam = ControllerMethodParam('user', String); - - return res.ok(actualParam.process(req, ctrlMethodParam)); - }); - - await (await request(app)) - .get('/234/hello') - .expectStatus(200) - .expectBody('234') - .test(); - }); - - test( - 'should use controller method property name if meta name not provided', - () async { - final app = pharaohWithErrorHdler - ..get('/boys/', (req, res) { - const ctrlMethodParam = ControllerMethodParam('user', String); - - final result = param.process(req, ctrlMethodParam); - return res.ok(result); - }); - - await (await request(app)) - .get('/boys/499') - .expectStatus(200) - .expectBody('499') - .test(); - }); - - test('when param value not valid', () async { - final app = pharaohWithErrorHdler - ..get('/test/', (req, res) { - const ctrlMethodParam = ControllerMethodParam('userId', int); - - final result = Param().process(req, ctrlMethodParam); - return res.ok(result.toString()); - }); - - await (await request(app)) - .get('/test/asfkd') - .expectStatus(422) - .expectJsonBody({ - 'location': 'param', - 'errors': ['userId must be a int type'] - }).test(); - - await (await request(app)) - .get('/test/2345') - .expectStatus(200) - .expectBody('2345') - .test(); - }); - }); - - group('Query', () { - test('should use name set in query', () async { - final app = pharaohWithErrorHdler - ..get('/foo', (req, res) { - final actualParam = Query('userId'); - const ctrlMethodParam = ControllerMethodParam('user', String); - - final result = actualParam.process(req, ctrlMethodParam); - return res.ok(result); - }); - - await (await request(app)) - .get('/foo?userId=Chima') - .expectStatus(200) - .expectBody('Chima') - .test(); - }); - - test( - 'should use controller method property name if Query name not provided', - () async { - final app = pharaohWithErrorHdler - ..get('/bar', (req, res) { - const ctrlMethodParam = ControllerMethodParam('userId', String); - - final result = query.process(req, ctrlMethodParam); - return res.ok(result); - }); - - await (await request(app)) - .get('/bar?userId=Precious') - .expectStatus(200) - .expectBody('Precious') - .test(); - }); - - test('when Query value not valid', () async { - final app = pharaohWithErrorHdler - ..get('/moo', (req, res) { - const ctrlMethodParam = ControllerMethodParam('name', int); - - final result = query.process(req, ctrlMethodParam); - return res.ok(result.toString()); - }); - - await (await request(app)) - .get('/moo?name=Chima') - .expectStatus(422) - .expectJsonBody({ - 'location': 'query', - 'errors': ['name must be a int type'] - }).test(); - - await (await request(app)) - .get('/moo') - .expectStatus(422) - .expectBody('{"location":"query","errors":["name is required"]}') - .test(); - - await (await request(app)) - .get('/moo?name=244') - .expectStatus(200) - .expectBody('244') - .test(); - }); - }); - - group('Header', () { - test('should use name set in meta', () async { - final app = pharaohWithErrorHdler - ..get('/foo', (req, res) { - final actualParam = Header(HttpHeaders.authorizationHeader); - const ctrlMethodParam = ControllerMethodParam('token', String); - - final result = actualParam.process(req, ctrlMethodParam); - return res.json(result); - }); - - await (await request(app)) - .get('/foo', headers: { - HttpHeaders.authorizationHeader: 'foo token', - }) - .expectStatus(200) - .expectJsonBody('[foo token]') - .test(); - }); - - test( - 'should use controller method property name if meta name not provided', - () async { - final app = pharaohWithErrorHdler - ..get('/bar', (req, res) { - final result = - header.process(req, ControllerMethodParam('token', String)); - return res.ok(result); - }); - - await (await request(app)) - .get('/bar', headers: {'token': 'Hello Token'}) - .expectStatus(200) - .expectBody('[Hello Token]') - .test(); - }); - - test('when Header value not valid', () async { - final app = pharaohWithErrorHdler - ..get('/moo', (req, res) { - final result = - header.process(req, ControllerMethodParam('age_max', String)); - return res.ok(result.toString()); - }); - - await (await request(app)) - .get('/moo', headers: {'age_max': 'Chima'}) - .expectStatus(200) - .expectBody('[Chima]') - .test(); - - await (await request(app)) - .get('/moo') - .expectStatus(422) - .expectBody( - '{"location":"header","errors":["age_max is required"]}') - .test(); - }); - }); - - group('Body', () { - test('should use name set in meta', () async { - final app = pharaohWithErrorHdler - ..post('/hello', (req, res) { - final actualParam = Body(); - final result = actualParam.process( - req, ControllerMethodParam('reqBody', dynamic)); - return res.json(result); - }); - await (await request(app)) - .post('/hello', {'foo': "bar"}) - .expectStatus(200) - .expectJsonBody({'foo': 'bar'}) - .test(); - }); - - test('when body not provided', () async { - final app = pharaohWithErrorHdler - ..post('/test', (req, res) { - final result = - body.process(req, ControllerMethodParam('reqBody', dynamic)); - return res.ok(result.toString()); - }); - - await (await request(app)) - .post('/test', null) - .expectStatus(422) - .expectJsonBody({ - 'location': 'body', - 'errors': ['body is required'] - }).test(); - - await (await request(app)) - .post('/test', {'hello': 'Foo'}) - .expectStatus(200) - .expectBody('{hello: Foo}') - .test(); - }); - - test('when dto provided', () async { - final dto = TestDTO(); - final testData = {'username': 'Foo', 'lastname': 'Bar', 'age': 22}; - - final app = pharaohWithErrorHdler - ..post('/mongo', (req, res) { - final actualParam = Body(); - final result = actualParam.process( - req, ControllerMethodParam('reqBody', TestDTO, dto: dto)); - return res - .json({'username': result is TestDTO ? result.username : null}); - }); - - await (await request(app)) - .post('/mongo', {}) - .expectStatus(422) - .expectJsonBody({ - 'location': 'body', - 'errors': [ - 'username: The field is required', - 'lastname: The field is required', - 'age: The field is required' - ] - }) - .test(); - - await (await request(app)) - .post('/mongo', testData) - .expectStatus(200) - .expectJsonBody({'username': 'Foo'}).test(); - }); - }); - }); -} diff --git a/packages/pharaoh/test/pharaoh_next/router_test.dart b/packages/pharaoh/test/pharaoh_next/router_test.dart deleted file mode 100644 index a8361a95..00000000 --- a/packages/pharaoh/test/pharaoh_next/router_test.dart +++ /dev/null @@ -1,228 +0,0 @@ -import 'package:pharaoh/pharaoh_next.dart'; -import 'package:spookie/spookie.dart'; - -import './router_test.reflectable.dart'; -import 'core/core_test.dart'; - -class TestController extends HTTPController { - void create() {} - - void index() {} - - void show() {} - - void update() {} - - void delete() {} -} - -void main() { - setUpAll(initializeReflectable); - - group('Router', () { - group('when route group', () { - test('with routes', () { - final group = Route.group('merchants', [ - Route.get('/get', (TestController, #index)), - Route.delete('/delete', (TestController, #delete)), - Route.put('/update', (TestController, #update)), - ]); - - expect(group.paths, [ - '[GET]: /merchants/get', - '[DELETE]: /merchants/delete', - '[PUT]: /merchants/update', - ]); - }); - - test('with prefix', () { - final group = Route.group( - 'Merchants', - [ - Route.get('/foo', (TestController, #index)), - Route.delete('/bar', (TestController, #delete)), - Route.put('/moo', (TestController, #update)), - ], - prefix: 'foo', - ); - - expect(group.paths, [ - '[GET]: /foo/foo', - '[DELETE]: /foo/bar', - '[PUT]: /foo/moo', - ]); - }); - - test('with handler', () { - final group = Route.group('users', [ - Route.route(HTTPMethod.GET, '/my-name', (req, res) => null), - ]); - expect(group.paths, ['[GET]: /users/my-name']); - }); - - test('with sub groups', () { - final group = Route.group('users', [ - Route.get('/get', (TestController, #index)), - Route.delete('/delete', (TestController, #delete)), - Route.put('/update', (TestController, #update)), - // - Route.group('customers', [ - Route.get('/foo', (TestController, #index)), - Route.delete('/bar', (TestController, #delete)), - Route.put('/set', (TestController, #update)), - ]), - ]); - - expect(group.paths, [ - '[GET]: /users/get', - '[DELETE]: /users/delete', - '[PUT]: /users/update', - '[GET]: /users/customers/foo', - '[DELETE]: /users/customers/bar', - '[PUT]: /users/customers/set', - ]); - }); - - group('when middlewares used', () { - test('should add to routes', () { - final group = Route.group('users', [ - Route.get('/get', (TestController, #index)), - Route.delete('/delete', (TestController, #delete)), - Route.put('/update', (TestController, #update)), - // - Route.group('customers', [ - Route.get('/foo', (TestController, #index)), - Route.delete('/bar', (TestController, #delete)), - Route.put('/set', (TestController, #update)), - ]), - ]); - - expect(group.paths, [ - '[GET]: /users/get', - '[DELETE]: /users/delete', - '[PUT]: /users/update', - '[GET]: /users/customers/foo', - '[DELETE]: /users/customers/bar', - '[PUT]: /users/customers/set', - ]); - }); - - test('should handle nested groups', () { - final group = Route.group('users', [ - Route.get('/get', (TestController, #index)), - Route.delete('/delete', (TestController, #delete)), - Route.put('/update', (TestController, #update)), - // - Route.group('customers', [ - Route.get('/foo', (TestController, #index)), - Route.delete('/bar', (TestController, #delete)), - Route.put('/set', (TestController, #update)), - ]), - ]); - - expect(group.paths, [ - '[GET]: /users/get', - '[DELETE]: /users/delete', - '[PUT]: /users/update', - '[GET]: /users/customers/foo', - '[DELETE]: /users/customers/bar', - '[PUT]: /users/customers/set', - ]); - }); - }); - - test('when handle route resource', () { - final group = - Route.group('foo', [Route.resource('bar', TestController)]) - ..middleware([ - (req, res, next) => next(), - (req, res, next) => next(), - ]); - - expect(group.paths, [ - '[ALL]: /foo', - '[GET]: /foo/bar', - '[GET]: /foo/bar/', - '[POST]: /foo/bar', - '[PUT]: /foo/bar/', - '[PATCH]: /foo/bar/', - '[DELETE]: /foo/bar/' - ]); - }); - - test('when handle route resource', () { - final group = Route.group('foo', [ - Route.resource('bar', TestController), - ]); - - expect(group.paths, [ - '[GET]: /foo/bar', - '[GET]: /foo/bar/', - '[POST]: /foo/bar', - '[PUT]: /foo/bar/', - '[PATCH]: /foo/bar/', - '[DELETE]: /foo/bar/' - ]); - }); - - test('when used with middleware', () { - TestKidsApp(); - - final group = Route.middleware('api').group('merchants', [ - Route.route(HTTPMethod.GET, '/create', (req, res) => null), - Route.group('users', [ - Route.get('/get', (TestController, #index)), - Route.delete('/delete', (TestController, #delete)), - Route.put('/update', (TestController, #update)), - Route.middleware('api').group('hello', [ - Route.get('/world', (TestController, #index)), - ]) - ]), - ]); - - expect(group.paths, [ - '[ALL]: /merchants', - '[GET]: /merchants/create', - '[GET]: /merchants/users/get', - '[DELETE]: /merchants/users/delete', - '[PUT]: /merchants/users/update', - '[ALL]: /merchants/users/hello', - '[GET]: /merchants/users/hello/world' - ]); - - var route = Route.middleware('api').routes([ - Route.get('/get', (TestController, #index)), - ]); - - expect(route.paths, ['[ALL]: /', '[GET]: /get']); - - route = Route.group('users', [route]); - expect(route.paths, ['[ALL]: /users', '[GET]: /users/get']); - - route = Route.group('admin', [ - route, - Route.middleware('api').routes([ - Route.get('/boys', (TestController, #index)), - ]) - ]); - - expect(route.paths, [ - '[ALL]: /admin/users', - '[GET]: /admin/users/get', - '[ALL]: /admin', - '[GET]: /admin/boys' - ]); - }); - }); - - test('should error when controller method not found', () { - expect( - () => Route.group( - 'Merchants', [Route.get('/foo', (TestController, #foobar))], - prefix: 'foo'), - throwsA(isA().having((p0) => p0.message, '', - 'TestController does not have method #foobar')), - ); - }); - }); -} diff --git a/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart b/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart deleted file mode 100644 index 9046713e..00000000 --- a/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'package:pharaoh/pharaoh_next.dart'; -import 'package:spookie/spookie.dart'; - -import 'validation_test.reflectable.dart'; - -class TestDTO extends BaseDTO { - String get username; - - String get lastname; - - int get age; -} - -class TestSingleOptional extends BaseDTO { - String get nationality; - - @ezOptional(String) - String? get address; - - @ezOptional(String, defaultValue: 'Ghana') - String get country; -} - -class DTOTypeMismatch extends BaseDTO { - @ezOptional(int) - String? get name; -} - -void main() { - initializeReflectable(); - - group('Validation', () { - group('when `ezRequired`', () { - test('when passed type as argument', () { - final requiredValidator = ezRequired(String).validator.build(); - expect(requiredValidator(null), 'The field is required'); - expect(requiredValidator(24), 'The field must be a String type'); - expect(requiredValidator('Foo'), isNull); - }); - - test('when passed type through generics', () { - final requiredValidator = ezRequired().validator.build(); - expect(requiredValidator(null), 'The field is required'); - expect(requiredValidator('Hello'), 'The field must be a int type'); - expect(requiredValidator(24), isNull); - }); - - test('when mis-matched types', () { - final requiredValidator = ezRequired().validator.build(); - expect(requiredValidator(null), 'The field is required'); - expect(requiredValidator('Hello'), 'The field must be a int type'); - expect(requiredValidator(24), isNull); - }); - }); - - test('when `ezOptional`', () { - final optionalValidator = ezOptional(String).validator.build(); - expect(optionalValidator(null), isNull); - expect(optionalValidator(24), 'The field must be a String type'); - expect(optionalValidator('Foo'), isNull); - }); - - test('when `ezEmail`', () { - final emailValidator = ezEmail().validator.build(); - expect(emailValidator('foo'), 'The field is not a valid email address'); - expect(emailValidator(24), 'The field must be a String type'); - expect(emailValidator('chima@yaroo.dev'), isNull); - }); - - test('when `ezMinLength`', () { - final val1 = ezMinLength(4).validator.build(); - expect(val1('foo'), 'The field must be at least 4 characters long'); - expect(val1('foob'), isNull); - expect(val1('foobd'), isNull); - }); - - test('when `ezMaxLength`', () { - final val1 = ezMaxLength(10).validator.build(); - expect(val1('foobasdfkasdfasdf'), - 'The field must be at most 10 characters long'); - expect(val1('foobasdfk'), isNull); - }); - - test('when `ezDateTime`', () { - var requiredValidator = ezDateTime().validator.build(); - final now = DateTime.now(); - expect(requiredValidator('foo'), 'The field must be a DateTime type'); - expect(requiredValidator(now), isNull); - expect(requiredValidator(null), 'The field is required'); - - requiredValidator = ezDateTime(optional: true).validator.build(); - expect(requiredValidator(null), isNull); - expect(requiredValidator('df'), 'The field must be a DateTime type'); - }); - }); - - group('when used in a class', () { - final pharaoh = Pharaoh() - ..onError((error, req, res) { - final actualError = error.exception; - if (actualError is RequestValidationError) { - return res.json(actualError.errorBody, statusCode: 422); - } - - return res.internalServerError(actualError.toString()); - }); - late Spookie appTester; - - setUpAll(() async => appTester = await request(pharaoh)); - - test('when no metas', () async { - final dto = TestDTO(); - final testData = {'username': 'Foo', 'lastname': 'Bar', 'age': 22}; - - final app = pharaoh - ..post('/', (req, res) { - dto.make(req); - return res.json({ - 'firstname': dto.username, - 'lastname': dto.lastname, - 'age': dto.age - }); - }); - - await appTester - .post('/', {}) - .expectStatus(422) - .expectJsonBody({ - 'location': 'body', - 'errors': [ - 'username: The field is required', - 'lastname: The field is required', - 'age: The field is required' - ] - }) - .test(); - - await (await request(app)) - .post('/', testData) - .expectStatus(200) - .expectJsonBody( - {'firstname': 'Foo', 'lastname': 'Bar', 'age': 22}).test(); - }); - - test('when single property optional', () async { - final dto = TestSingleOptional(); - - final app = pharaoh - ..post('/optional', (req, res) { - dto.make(req); - - return res.json({ - 'nationality': dto.nationality, - 'address': dto.address, - 'country': dto.country - }); - }); - - await (await request(app)) - .post('/optional', {}) - .expectStatus(422) - .expectJsonBody({ - 'location': 'body', - 'errors': ['nationality: The field is required'] - }) - .test(); - - await (await request(app)) - .post('/optional', {'nationality': 'Ghanaian'}) - .expectStatus(200) - .expectJsonBody( - {'nationality': 'Ghanaian', 'address': null, 'country': 'Ghana'}) - .test(); - - await (await request(app)) - .post('/optional', {'nationality': 'Ghanaian', 'address': 344}) - .expectStatus(422) - .expectJsonBody({ - 'location': 'body', - 'errors': ['address: The field must be a String type'] - }) - .test(); - - await (await request(app)) - .post('/optional', - {'nationality': 'Ghanaian', 'address': 'Terminalia Street'}) - .expectStatus(200) - .expectJsonBody({ - 'nationality': 'Ghanaian', - 'address': 'Terminalia Street', - 'country': 'Ghana' - }) - .test(); - }); - - test('when type mismatch', () async { - final dto = DTOTypeMismatch(); - - pharaoh.post('/type-mismatch', (req, res) { - dto.make(req); - return res.ok('Foo Bar'); - }); - - await (await request(pharaoh)) - .post('/type-mismatch', {'name': 'Chima'}) - .expectStatus(500) - .expectJsonBody({ - 'error': - 'Invalid argument(s): Type Mismatch between ezOptional(int) & DTOTypeMismatch class property name->(String)' - }) - .test(); - }); - }); -} From db2a4b4e39d2d31475fc92c4f6ab71d656f4256c Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Fri, 4 Jul 2025 17:46:11 +0000 Subject: [PATCH 02/10] remove shelf interop --- packages/pharaoh/lib/pharaoh.dart | 3 - packages/pharaoh/lib/src/http/body.dart | 95 +++++++++++++++++++ packages/pharaoh/lib/src/http/message.dart | 4 +- packages/pharaoh/lib/src/http/response.dart | 7 +- .../pharaoh/lib/src/http/response_impl.dart | 10 +- packages/pharaoh/lib/src/http/router.dart | 4 +- .../lib/src/shelf_interop/adapter.dart | 71 -------------- .../pharaoh/lib/src/shelf_interop/shelf.dart | 6 -- packages/pharaoh/lib/src/view/view.dart | 4 +- 9 files changed, 109 insertions(+), 95 deletions(-) create mode 100644 packages/pharaoh/lib/src/http/body.dart delete mode 100644 packages/pharaoh/lib/src/shelf_interop/adapter.dart delete mode 100644 packages/pharaoh/lib/src/shelf_interop/shelf.dart diff --git a/packages/pharaoh/lib/pharaoh.dart b/packages/pharaoh/lib/pharaoh.dart index e863d89f..07b66ec2 100644 --- a/packages/pharaoh/lib/pharaoh.dart +++ b/packages/pharaoh/lib/pharaoh.dart @@ -6,9 +6,6 @@ export 'src/http/request.dart'; export 'src/http/response.dart'; export 'src/http/router.dart'; -export 'src/shelf_interop/adapter.dart'; -export 'src/shelf_interop/shelf.dart' show ShelfBody; - export 'src/utils/utils.dart'; export 'src/utils/exceptions.dart'; diff --git a/packages/pharaoh/lib/src/http/body.dart b/packages/pharaoh/lib/src/http/body.dart new file mode 100644 index 00000000..db6a126b --- /dev/null +++ b/packages/pharaoh/lib/src/http/body.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; + +/// The body of a request or response. +/// +/// This tracks whether the body has been read. It's separate from [Message] +/// because the message may be changed with [Message.change], but each instance +/// should share a notion of whether the body was read. +class Body { + /// The contents of the message body. + /// + /// This will be `null` after [read] is called. + Stream>? _stream; + + /// The encoding used to encode the stream returned by [read], or `null` if no + /// encoding was used. + final Encoding? encoding; + + /// The length of the stream returned by [read], or `null` if that can't be + /// determined efficiently. + final int? contentLength; + + Body._(this._stream, this.encoding, this.contentLength); + + /// Converts [body] to a byte stream and wraps it in a [Body]. + /// + /// [body] may be either a [Body], a [String], a [List], a + /// [Stream>], or `null`. If it's a [String], [encoding] will be + /// used to convert it to a [Stream>]. + factory Body(Object? body, [Encoding? encoding]) { + if (body is Body) return body; + + Stream> stream; + int? contentLength; + if (body == null) { + contentLength = 0; + stream = Stream.fromIterable([]); + } else if (body is String) { + if (encoding == null) { + var encoded = utf8.encode(body); + // If the text is plain ASCII, don't modify the encoding. This means + // that an encoding of "text/plain" will stay put. + if (!_isPlainAscii(encoded, body.length)) encoding = utf8; + contentLength = encoded.length; + stream = Stream.fromIterable([encoded]); + } else { + var encoded = encoding.encode(body); + contentLength = encoded.length; + stream = Stream.fromIterable([encoded]); + } + } else if (body is List) { + // Avoid performance overhead from an unnecessary cast. + contentLength = body.length; + stream = Stream.value(body); + } else if (body is List) { + contentLength = body.length; + stream = Stream.value(body.cast()); + } else if (body is Stream>) { + // Avoid performance overhead from an unnecessary cast. + stream = body; + } else if (body is Stream) { + stream = body.cast(); + } else { + throw ArgumentError('Response body "$body" must be a String or a ' + 'Stream.'); + } + + return Body._(stream, encoding, contentLength); + } + + /// Returns whether [bytes] is plain ASCII. + /// + /// [codeUnits] is the number of code units in the original string. + static bool _isPlainAscii(List bytes, int codeUnits) { + // Most non-ASCII code units will produce multiple bytes and make the text + // longer. + if (bytes.length != codeUnits) return false; + + // Non-ASCII code units between U+0080 and U+009F produce 8-bit characters + // with the high bit set. + return bytes.every((byte) => byte & 0x80 == 0); + } + + /// Returns a [Stream] representing the body. + /// + /// Can only be called once. + Stream> read() { + if (_stream == null) { + throw StateError("The 'read' method can only be called once on a " + 'shelf.Request/shelf.Response object.'); + } + var stream = _stream!; + _stream = null; + return stream; + } +} diff --git a/packages/pharaoh/lib/src/http/message.dart b/packages/pharaoh/lib/src/http/message.dart index 0ea7c173..47762b15 100644 --- a/packages/pharaoh/lib/src/http/message.dart +++ b/packages/pharaoh/lib/src/http/message.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:http_parser/http_parser.dart'; -import '../shelf_interop/shelf.dart'; +import 'body.dart'; abstract class Message { final Map headers; @@ -44,6 +44,6 @@ abstract class Message { int? get contentLength { final content = body; - return content is ShelfBody ? content.contentLength : null; + return content is Body ? content.contentLength : null; } } diff --git a/packages/pharaoh/lib/src/http/response.dart b/packages/pharaoh/lib/src/http/response.dart index 00ea4f6c..ca3a7a7c 100644 --- a/packages/pharaoh/lib/src/http/response.dart +++ b/packages/pharaoh/lib/src/http/response.dart @@ -3,14 +3,13 @@ import 'dart:io'; import 'package:pharaoh/pharaoh.dart'; import 'package:http_parser/http_parser.dart'; - -import '../shelf_interop/shelf.dart' as shelf; +import 'package:pharaoh/src/http/body.dart'; import 'message.dart'; part 'response_impl.dart'; -sealed class Response extends Message { +sealed class Response extends Message { Response(super.body, {super.headers = const {}}); /// Constructs an HTTP Response @@ -21,7 +20,7 @@ sealed class Response extends Message { Map? headers, }) => _$ResponseImpl._( - body: body == null ? null : ShelfBody(body), + body: body == null ? null : Body(body), ended: false, statusCode: statusCode, headers: headers ?? {}, diff --git a/packages/pharaoh/lib/src/http/response_impl.dart b/packages/pharaoh/lib/src/http/response_impl.dart index 1e19c928..0ee88a86 100644 --- a/packages/pharaoh/lib/src/http/response_impl.dart +++ b/packages/pharaoh/lib/src/http/response_impl.dart @@ -40,7 +40,7 @@ class _$ResponseImpl extends Response { /// /// [statusCode] must be greater than or equal to 100. _$ResponseImpl._({ - shelf.ShelfBody? body, + Body? body, int? statusCode, this.ended = false, Map headers = const {}, @@ -77,7 +77,7 @@ class _$ResponseImpl extends Response { ); @override - Response withBody(Object object) => this..body = shelf.ShelfBody(object); + Response withBody(Object object) => this..body = Body(object); @override _$ResponseImpl redirect(String url, [int statusCode = HttpStatus.found]) { @@ -123,7 +123,7 @@ class _$ResponseImpl extends Response { statusCode = HttpStatus.internalServerError; } - body = shelf.ShelfBody(result); + body = Body(result); return this.status(statusCode).end(); } @@ -144,13 +144,13 @@ class _$ResponseImpl extends Response { @override _$ResponseImpl ok([String? data]) => this.end() ..headers[HttpHeaders.contentTypeHeader] = ContentType.text.toString() - ..body = shelf.ShelfBody(data, encoding); + ..body = Body(data, encoding); @override _$ResponseImpl send(Object data) { return this.end() ..headers[HttpHeaders.contentTypeHeader] ??= ContentType.binary.toString() - ..body = shelf.ShelfBody(data); + ..body = Body(data); } @override diff --git a/packages/pharaoh/lib/src/http/router.dart b/packages/pharaoh/lib/src/http/router.dart index 330ba536..2893eb72 100644 --- a/packages/pharaoh/lib/src/http/router.dart +++ b/packages/pharaoh/lib/src/http/router.dart @@ -9,10 +9,10 @@ import 'package:spanner/src/tree/tree.dart' show BASE_PATH; import '../middleware/body_parser.dart'; import '../middleware/session_mw.dart'; -import '../shelf_interop/shelf.dart' as shelf; import '../utils/exceptions.dart'; import '../view/view.dart'; +import 'body.dart'; import 'request.dart'; import 'response.dart'; @@ -181,7 +181,7 @@ class _$PharaohImpl extends RouterContract // // TODO(codekeyz): Do this more cleanly when sdk#27886 is fixed. final newStream = chunkedCoding.decoder.bind(res_.body!.read()); - res_.body = shelf.ShelfBody(newStream); + res_.body = Body(newStream); request.headers.set(HttpHeaders.transferEncodingHeader, 'chunked'); } else if (statusCode >= 200 && statusCode != 204 && diff --git a/packages/pharaoh/lib/src/shelf_interop/adapter.dart b/packages/pharaoh/lib/src/shelf_interop/adapter.dart deleted file mode 100644 index 7fd1f094..00000000 --- a/packages/pharaoh/lib/src/shelf_interop/adapter.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import '../http/request.dart'; -import '../http/response.dart'; -import '../http/router.dart'; -import '../utils/exceptions.dart'; -import 'shelf.dart' as shelf; - -typedef ShelfMiddlewareType2 = FutureOr Function(shelf.Request); - -/// Use this hook to transform any shelf -/// middleware into a [Middleware] that Pharaoh -/// can use. -/// -/// This will also throw an Exception if you use a Middleware -/// that has a [Type] signature different from either [shelf.Middleware] -/// or [ShelfMiddlewareType2] tho in most cases, it should work. -Middleware useShelfMiddleware(dynamic middleware) { - if (middleware is shelf.Middleware) { - return (req, res, next) async { - final shelfResponse = await middleware( - (req) => shelf.Response.ok(req.read()))(_toShelfRequest(req)); - res = _fromShelfResponse((req: req, res: res), shelfResponse); - - next(res); - }; - } - - if (middleware is ShelfMiddlewareType2) { - return (req, res, next) async { - final shelfResponse = await middleware(_toShelfRequest(req)); - res = _fromShelfResponse((req: req, res: res), shelfResponse); - - /// TODO(codekeyz) find out how to end or let the request continue - /// based off the shelf response - next(res.end()); - }; - } - - throw PharaohException.value('Unknown Shelf Middleware Type', middleware); -} - -shelf.Request _toShelfRequest(Request req) { - final httpReq = req.actual; - - var headers = >{}; - httpReq.headers.forEach((k, v) { - headers[k] = v; - }); - - return shelf.Request( - httpReq.method, - httpReq.requestedUri, - protocolVersion: httpReq.protocolVersion, - headers: headers, - body: httpReq, - context: {'shelf.io.connection_info': httpReq.connectionInfo!}, - ); -} - -Response _fromShelfResponse(ReqRes reqRes, shelf.Response response) { - Map headers = reqRes.res.headers; - response.headers.forEach((key, value) => headers[key] = value); - - return Response.create( - body: response.read(), - encoding: response.encoding, - headers: headers, - statusCode: response.statusCode, - ); -} diff --git a/packages/pharaoh/lib/src/shelf_interop/shelf.dart b/packages/pharaoh/lib/src/shelf_interop/shelf.dart deleted file mode 100644 index 90da6379..00000000 --- a/packages/pharaoh/lib/src/shelf_interop/shelf.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:shelf/src/body.dart'; -export 'package:shelf/src/request.dart'; -export 'package:shelf/src/response.dart'; -export 'package:shelf/src/middleware.dart'; - -typedef ShelfBody = Body; diff --git a/packages/pharaoh/lib/src/view/view.dart b/packages/pharaoh/lib/src/view/view.dart index d218eee3..7f2f12aa 100644 --- a/packages/pharaoh/lib/src/view/view.dart +++ b/packages/pharaoh/lib/src/view/view.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:isolate'; +import '../http/body.dart'; import '../http/router.dart'; import '../utils/exceptions.dart'; -import '../shelf_interop/shelf.dart' as shelf; abstract class ViewEngine { String get name; @@ -30,7 +30,7 @@ final viewRenderHook = RequestHook( final result = await Isolate.run( () => viewEngine.render(viewData.name, viewData.data), ); - res = res.end()..body = shelf.ShelfBody(result); + res = res.end()..body = Body(result); } catch (e) { throw PharaohException.value('Failed to render view ${viewData.name}', e); } From 63429496ed67a70d9f28e137ec55aa27ff4d1942 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Fri, 4 Jul 2025 17:55:17 +0000 Subject: [PATCH 03/10] revert back to one part at a time --- packages/spanner/lib/src/tree/tree.dart | 41 +++++++------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/packages/spanner/lib/src/tree/tree.dart b/packages/spanner/lib/src/tree/tree.dart index 01d398ff..4189abb8 100644 --- a/packages/spanner/lib/src/tree/tree.dart +++ b/packages/spanner/lib/src/tree/tree.dart @@ -89,51 +89,32 @@ class Spanner { HandlerStore _on(HTTPMethod method, String path) { final pathSegments = getRoutePathSegments(path); - Node rootNode = _root; if (pathSegments.isEmpty) { return rootNode; - } else if (pathSegments[0] == WildcardNode.key) { + } + + if (pathSegments[0] == WildcardNode.key) { return rootNode.wildcardNode ?? rootNode.addChildAndReturn(WildcardNode.key, WildcardNode()); } - for (int i = 0; i < pathSegments.length; i += 2) { - final firstPart = pathSegments[i]; - final secondPart = - i + 1 < pathSegments.length ? pathSegments[i + 1] : null; - final thirdPart = - i + 2 < pathSegments.length ? pathSegments[i + 2] : null; + for (int i = 0; i < pathSegments.length; i++) { + final current = pathSegments[i]; + final next = i + 1 < pathSegments.length ? pathSegments[i + 1] : null; - final first = Spanner._computeNode( + final result = Spanner._computeNode( rootNode, method, - firstPart, + current, config: config, fullPath: path, - nextPart: secondPart, + nextPart: next, ); - /// the only time [result] won't be Node is when we have a parametric definition - if (first is! Node) return first; - - rootNode = first; - - if (secondPart != null) { - final second = Spanner._computeNode( - rootNode, - method, - secondPart, - config: config, - fullPath: path, - nextPart: thirdPart, - ); - - if (second is! Node) return second; - - rootNode = second; - } + if (result is! Node) return result; + rootNode = result; } return rootNode; From f01954d81240652d9523373393a282a1f42583bc Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Sun, 6 Jul 2025 17:47:43 +0000 Subject: [PATCH 04/10] remove unused props --- .../lib/src/parametric/definition.dart | 35 +++++----------- .../spanner/lib/src/parametric/utils.dart | 27 +++++-------- packages/spanner/lib/src/tree/node.dart | 7 +--- packages/spanner/lib/src/tree/tree.dart | 40 +++++++------------ 4 files changed, 36 insertions(+), 73 deletions(-) diff --git a/packages/spanner/lib/src/parametric/definition.dart b/packages/spanner/lib/src/parametric/definition.dart index 1b894418..4e4586c1 100644 --- a/packages/spanner/lib/src/parametric/definition.dart +++ b/packages/spanner/lib/src/parametric/definition.dart @@ -39,11 +39,7 @@ abstract class ParameterDefinition implements HandlerStore { bool matches(String route, {bool caseSensitive = false}); - void resolveParams( - String pattern, - List collector, { - bool caseSentive = false, - }); + void resolveParams(String pattern, List collector); } class SingleParameterDefn extends ParameterDefinition with HandlerStoreMixin { @@ -66,12 +62,7 @@ class SingleParameterDefn extends ParameterDefinition with HandlerStoreMixin { @override bool matches(String route, {bool caseSensitive = false}) { - final match = matchPattern( - route, - prefix ?? '', - suffix ?? '', - caseSensitive, - ); + final match = matchPattern(route, prefix ?? '', suffix ?? ''); return match != null; } @@ -84,15 +75,13 @@ class SingleParameterDefn extends ParameterDefinition with HandlerStoreMixin { _terminal = false; @override - void resolveParams( - final String pattern, - List collector, { - bool caseSentive = false, - }) { - collector.add(( - name: name, - value: matchPattern(pattern, prefix ?? "", suffix ?? "", caseSentive) - )); + void resolveParams(final String pattern, List collector) { + collector.add( + ( + name: name, + value: matchPattern(pattern, prefix ?? "", suffix ?? ""), + ), + ); } @override @@ -128,11 +117,7 @@ class CompositeParameterDefinition extends ParameterDefinition _template.hasMatch(route); @override - void resolveParams( - String pattern, - List collector, { - bool caseSentive = false, - }) { + void resolveParams(String pattern, List collector) { final match = _template.firstMatch(pattern); if (match == null) return; diff --git a/packages/spanner/lib/src/parametric/utils.dart b/packages/spanner/lib/src/parametric/utils.dart index 3fd49fb3..fef5b84a 100644 --- a/packages/spanner/lib/src/parametric/utils.dart +++ b/packages/spanner/lib/src/parametric/utils.dart @@ -113,12 +113,12 @@ const _upperA = 65; // 'A' const _lowerZ = 122; // 'z' const _upperZ = 90; // 'Z' -int _stringToBitmask(String s, bool caseSensitive) { +int _stringToBitmask(String s) { int mask = 0; for (int i = 0; i < s.length; i++) { int charCode = s.codeUnitAt(i); - if (!caseSensitive && charCode >= _upperA && charCode <= _upperZ) { + if (charCode >= _upperA && charCode <= _upperZ) { charCode += 32; // Convert to lowercase } if (charCode >= _lowerA && charCode <= _lowerZ) { @@ -128,29 +128,23 @@ int _stringToBitmask(String s, bool caseSensitive) { return mask; } -String? matchPattern( - String input, - String prefix, - String suffix, - bool caseSensitive, -) { +String? matchPattern(String input, String prefix, String suffix) { if (prefix.isEmpty && suffix.isEmpty) return input; - final prefixMask = _stringToBitmask(prefix, caseSensitive); - final suffixMask = _stringToBitmask(suffix, caseSensitive); + final prefixMask = _stringToBitmask(prefix); + final suffixMask = _stringToBitmask(suffix); int matchStart = 0; int matchEnd = input.length; - final compareInput = caseSensitive ? input : input.toLowerCase(); - final comparePrefix = caseSensitive ? prefix : prefix.toLowerCase(); - final compareSuffix = caseSensitive ? suffix : suffix.toLowerCase(); + final compareInput = input.toLowerCase(); + final comparePrefix = prefix.toLowerCase(); + final compareSuffix = suffix.toLowerCase(); if (prefix.isNotEmpty) { bool prefixFound = false; for (int i = 0; i <= input.length - prefix.length; i++) { - if (_stringToBitmask( - compareInput.substring(i, i + prefix.length), caseSensitive) == + if (_stringToBitmask(compareInput.substring(i, i + prefix.length)) == prefixMask) { if (compareInput.substring(i, i + prefix.length) == comparePrefix) { matchStart = i + prefix.length; @@ -165,8 +159,7 @@ String? matchPattern( if (suffix.isNotEmpty) { bool suffixFound = false; for (int i = input.length - suffix.length; i >= matchStart; i--) { - if (_stringToBitmask( - compareInput.substring(i, i + suffix.length), caseSensitive) == + if (_stringToBitmask(compareInput.substring(i, i + suffix.length)) == suffixMask) { if (compareInput.substring(i, i + suffix.length) == compareSuffix) { matchEnd = i; diff --git a/packages/spanner/lib/src/tree/node.dart b/packages/spanner/lib/src/tree/node.dart index 2b691362..7d451c89 100644 --- a/packages/spanner/lib/src/tree/node.dart +++ b/packages/spanner/lib/src/tree/node.dart @@ -7,7 +7,7 @@ import '../parametric/utils.dart'; part '../route/action.dart'; -abstract class Node with HandlerStoreMixin { +sealed class Node with HandlerStoreMixin { final Map _nodesMap; Node() : _nodesMap = {}; @@ -191,13 +191,10 @@ class ParametricNode extends Node { HTTPMethod method, String part, { bool terminal = false, - bool caseSensitive = false, - String? nextPart, }) { return _definitionsMap[method]?.firstWhereOrNull( (e) => - (!terminal || (e.terminal && e.hasMethod(method))) && - e.matches(part, caseSensitive: caseSensitive), + (!terminal || (e.terminal && e.hasMethod(method))) && e.matches(part), ); } } diff --git a/packages/spanner/lib/src/tree/tree.dart b/packages/spanner/lib/src/tree/tree.dart index 4189abb8..436943c0 100644 --- a/packages/spanner/lib/src/tree/tree.dart +++ b/packages/spanner/lib/src/tree/tree.dart @@ -11,12 +11,10 @@ const BASE_PATH = '/'; enum HTTPMethod { GET, HEAD, POST, PUT, DELETE, ALL, PATCH, OPTIONS, TRACE } class RouterConfig { - final String rootPath; final bool caseSensitive; final bool ignoreTrailingSlash; const RouterConfig({ - this.rootPath = BASE_PATH, this.caseSensitive = true, this.ignoreTrailingSlash = true, }); @@ -32,8 +30,7 @@ class Spanner { int get _nextIndex => _currentIndex + 1; - Spanner({this.config = const RouterConfig()}) - : _root = StaticNode(config.rootPath); + Spanner({this.config = const RouterConfig()}) : _root = StaticNode(BASE_PATH); void addRoute(HTTPMethod method, String path, T handler) { _on(method, path).addRoute(method, ( @@ -88,29 +85,25 @@ class Spanner { } HandlerStore _on(HTTPMethod method, String path) { - final pathSegments = getRoutePathSegments(path); + final parts = getRoutePathSegments(path); Node rootNode = _root; - if (pathSegments.isEmpty) { + if (parts.isEmpty) { return rootNode; } - if (pathSegments[0] == WildcardNode.key) { + if (parts[0] == WildcardNode.key) { return rootNode.wildcardNode ?? rootNode.addChildAndReturn(WildcardNode.key, WildcardNode()); } - for (int i = 0; i < pathSegments.length; i++) { - final current = pathSegments[i]; - final next = i + 1 < pathSegments.length ? pathSegments[i + 1] : null; - + for (int index = 0; index < parts.length; index++) { final result = Spanner._computeNode( rootNode, method, - current, + index, + parts: parts, config: config, - fullPath: path, - nextPart: next, ); if (result is! Node) return result; @@ -137,13 +130,13 @@ class Spanner { static HandlerStore _computeNode( Node node, HTTPMethod method, - String routePart, { + int index, { + required List parts, required RouterConfig config, - required String? nextPart, - required String fullPath, }) { + final routePart = parts[index]; + final nextPart = parts.elementAtOrNull(index + 1); final part = config.caseSensitive ? routePart : routePart.toLowerCase(); - final child = node.maybeChild(part); if (child != null) { return node.addChildAndReturn(part, child); @@ -152,7 +145,7 @@ class Spanner { } else if (part.isWildCard) { if (nextPart != null) { throw ArgumentError.value( - fullPath, + parts.join('/'), null, 'Route definition is not valid. Wildcard must be the end of the route', ); @@ -184,6 +177,7 @@ class Spanner { final resolvedParams = []; final resolvedHandlers = [...root.middlewares]; + @pragma('vm:prefer-inline') getResults(IndexedValue? handler) => handler != null ? (resolvedHandlers..add(handler)) : resolvedHandlers; @@ -230,8 +224,6 @@ class Spanner { method, routePart, terminal: isLastPart, - caseSensitive: config.caseSensitive, - nextPart: isLastPart ? null : pathSegments[i + 1], ); /// If we don't find a matching path or a matching definition, then @@ -246,11 +238,7 @@ class Spanner { continue; } - definition!.resolveParams( - currPart, - resolvedParams, - caseSentive: config.caseSensitive, - ); + definition!.resolveParams(currPart, resolvedParams); if (isLastPart && definition.terminal) { return RouteResult( From bade7d06ae462b71d93115f76a34355ee61eefcb Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Sun, 6 Jul 2025 18:21:29 +0000 Subject: [PATCH 05/10] include early breaks --- packages/spanner/lib/src/tree/tree.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/spanner/lib/src/tree/tree.dart b/packages/spanner/lib/src/tree/tree.dart index 436943c0..a18ddda8 100644 --- a/packages/spanner/lib/src/tree/tree.dart +++ b/packages/spanner/lib/src/tree/tree.dart @@ -204,6 +204,11 @@ class Spanner { final childNode = rootNode.maybeChild(routePart) ?? parametricNode?.maybeChild(routePart); + if (childNode is StaticNode && isLastPart) { + rootNode = childNode; + break; + } + wildcardNode = childNode?.wildcardNode ?? wildcardNode; if (childNode == null && parametricNode == null) { @@ -211,11 +216,8 @@ class Spanner { return RouteResult(resolvedParams, getResults(null)); } - return RouteResult( - resolvedParams, - getResults(wildcardNode.getHandler(method)), - actual: wildcardNode, - ); + rootNode = wildcardNode; + break; } rootNode = (childNode ?? parametricNode)!; @@ -249,6 +251,8 @@ class Spanner { } } + resolvedHandlers.addAll(rootNode.middlewares); + return !rootNode.terminal ? RouteResult(resolvedParams, getResults(null), actual: null) : RouteResult( From c547710590eca130d2fd605a56f5bde35212c88d Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Thu, 10 Jul 2025 20:57:11 +0000 Subject: [PATCH 06/10] _ --- packages/pharaoh/pubspec.yaml | 10 --------- pharaoh_examples/lib/serve_files_2/index.dart | 22 ------------------- .../lib/shelf_middleware/cors.dart | 13 ----------- .../lib/shelf_middleware/helmet.dart | 13 ----------- 4 files changed, 58 deletions(-) delete mode 100644 pharaoh_examples/lib/serve_files_2/index.dart delete mode 100644 pharaoh_examples/lib/shelf_middleware/cors.dart delete mode 100644 pharaoh_examples/lib/shelf_middleware/helmet.dart diff --git a/packages/pharaoh/pubspec.yaml b/packages/pharaoh/pubspec.yaml index 4cbc0709..4edb420f 100644 --- a/packages/pharaoh/pubspec.yaml +++ b/packages/pharaoh/pubspec.yaml @@ -14,16 +14,6 @@ dependencies: http_parser: ^4.0.2 crypto: ^3.0.3 uuid: ^4.2.1 - - # We only need this to implement inter-operability for shelf - shelf: ^1.4.1 - - # framework - reflectable: ^4.0.12 - get_it: ^8.0.2 - grammer: ^1.0.3 - dotenv: ^4.2.0 - ez_validator_dart: ^0.3.1 spookie: ^1.0.2+3 dev_dependencies: diff --git a/pharaoh_examples/lib/serve_files_2/index.dart b/pharaoh_examples/lib/serve_files_2/index.dart deleted file mode 100644 index 2b23c845..00000000 --- a/pharaoh_examples/lib/serve_files_2/index.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:pharaoh/pharaoh.dart'; -import 'package:shelf_static/shelf_static.dart'; -import 'package:shelf_cors_headers/shelf_cors_headers.dart'; - -final app = Pharaoh(); - -final serveStatic = createStaticHandler( - 'public/web_demo_2', - defaultDocument: 'index.html', -); - -final cors = corsHeaders(); - -void main() async { - app.useRequestHook(logRequestHook); - - app.use(useShelfMiddleware(cors)); - - app.use(useShelfMiddleware(serveStatic)); - - await app.listen(); -} diff --git a/pharaoh_examples/lib/shelf_middleware/cors.dart b/pharaoh_examples/lib/shelf_middleware/cors.dart deleted file mode 100644 index 15b79d5c..00000000 --- a/pharaoh_examples/lib/shelf_middleware/cors.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:pharaoh/pharaoh.dart'; -import 'package:shelf_cors_headers/shelf_cors_headers.dart'; - -final app = Pharaoh(); - -void main() async { - /// Using shelf_cors_header with Pharaoh - app.use(useShelfMiddleware(corsHeaders())); - - app.get('/', (req, res) => res.json(req.headers)); - - await app.listen(); -} diff --git a/pharaoh_examples/lib/shelf_middleware/helmet.dart b/pharaoh_examples/lib/shelf_middleware/helmet.dart deleted file mode 100644 index 8d85e21c..00000000 --- a/pharaoh_examples/lib/shelf_middleware/helmet.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:pharaoh/pharaoh.dart'; -import 'package:shelf_helmet/shelf_helmet.dart'; - -final app = Pharaoh(); - -void main() async { - /// Using shelf_helmet with Pharaoh - app.use(useShelfMiddleware(helmet())); - - app.get('/', (req, res) => res.json(req.headers)); - - await app.listen(); -} From c0805ee90f22212f090f5f8a12085a6cc71496b3 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Thu, 10 Jul 2025 21:00:31 +0000 Subject: [PATCH 07/10] _ --- .github/workflows/test.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4734486a..7ad7bc5c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,9 +31,7 @@ jobs: run: melos format -- --set-exit-if-changed - name: Check linting - run: | - cd packages/pharaoh && dart run build_runner build --delete-conflicting-outputs - melos analyze + run: melos analyze test: name: Test Packages From 128f93b4f6adf5da3060f1d595b428219e10abb5 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Thu, 10 Jul 2025 21:03:04 +0000 Subject: [PATCH 08/10] _ --- .github/workflows/test.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7ad7bc5c..da9a0b6d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -42,13 +42,7 @@ jobs: - uses: dart-lang/setup-dart@v1.3 - uses: bluefireteam/melos-action@v3 - - - name: Bootstrap - run: | - dart pub global activate melos - melos bootstrap - cd packages/pharaoh && dart run build_runner build --delete-conflicting-outputs - + - name: Run Unit tests run: melos tests:ci From c125617fde9ef0cfe9801bd196a18c4cffeb29f9 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Sat, 12 Jul 2025 14:27:31 +0000 Subject: [PATCH 09/10] add cache for route segments --- packages/spanner/lib/src/tree/tree.dart | 38 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/spanner/lib/src/tree/tree.dart b/packages/spanner/lib/src/tree/tree.dart index a18ddda8..552c9ea5 100644 --- a/packages/spanner/lib/src/tree/tree.dart +++ b/packages/spanner/lib/src/tree/tree.dart @@ -262,21 +262,37 @@ class Spanner { ); } - List getRoutePathSegments(dynamic route) { - if (route is Uri) return route.pathSegments; - if (route == BASE_PATH) return const []; - if (route == WildcardNode.key) return const [WildcardNode.key]; - - var path = route.toString(); - if (path.isEmpty) return const []; + final _pathCache = >{}; + + @pragma('vm:prefer-inline') + List getRoutePathSegments(Object route) { + final cached = _pathCache[route]; + if (cached != null) return cached; + + late final List segments; + + if (route is Uri) { + segments = route.pathSegments; + } else if (identical(route, BASE_PATH)) { + segments = const []; + } else if (identical(route, WildcardNode.key)) { + segments = const [WildcardNode.key]; + } else { + var path = route.toString(); + if (path.isEmpty) { + segments = const []; + } else { + final start = path.startsWith(BASE_PATH) ? 1 : 0; + final end = path.endsWith(BASE_PATH) ? path.length - 1 : path.length; + segments = path.substring(start, end).split('/'); + } + } - if (path.startsWith(BASE_PATH)) path = path.substring(1); - if (path.endsWith(BASE_PATH)) path = path.substring(0, path.length - 1); - return path.split('/'); + return _pathCache[route] = segments; } } -class RouteResult { +final class RouteResult { final List _params; final List _values; From 812fd75a16707358ef2c7ce0ca808c7888f3e244 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Sat, 12 Jul 2025 14:42:40 +0000 Subject: [PATCH 10/10] _ --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 68a84738..27ab6c81 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # Pharaoh 🏇 -[![Dart](https://github.com/codekeyz/pharaoh/workflows/Dart/badge.svg)](https://github.com/codekeyz/pharaoh/actions/workflows/test.yml) +[![Test Pipeline](https://github.com/codekeyz/pharaoh/actions/workflows/test.yaml/badge.svg)](https://github.com/codekeyz/pharaoh/actions/workflows/test.yaml) [![codecov](https://codecov.io/gh/codekeyz/pharaoh/graph/badge.svg?token=4CJTGP1U2M)](https://codecov.io/gh/codekeyz/pharaoh) [![Pub Version](https://img.shields.io/pub/v/pharaoh?color=green)](https://pub.dev/packages/pharaoh) -[![popularity](https://img.shields.io/pub/popularity/pharaoh?logo=dart)](https://pub.dev/packages/pharaoh/score) [![likes](https://img.shields.io/pub/likes/pharaoh?logo=dart)](https://pub.dev/packages/pharaoh/score) [![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos)