-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement pharaoh framework (#120)
* implement pharaoh next * add tests for pharaoh next * fix lint issues * ignore generated files * run generator * damn * :) * tiny fix
- Loading branch information
Showing
30 changed files
with
2,173 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:dotenv/dotenv.dart'; | ||
|
||
DotEnv? _env; | ||
|
||
T env<T extends Object>(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<String>) => jsonDecode(strVal), | ||
_ => throw ArgumentError.value( | ||
T, null, 'Unsupported Type used in `env` call.'), | ||
}; | ||
return parsedVal as T; | ||
} | ||
|
||
extension ConfigExtension on Map<String, dynamic> { | ||
T getValue<T>(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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import 'package:get_it/get_it.dart'; | ||
|
||
final GetIt _getIt = GetIt.instance; | ||
|
||
T instanceFromRegistry<T extends Object>({Type? type}) { | ||
type ??= T; | ||
try { | ||
return _getIt.get(type: type) as T; | ||
} catch (_) { | ||
throw Exception('Dependency not found in registry: $type'); | ||
} | ||
} | ||
|
||
T registerSingleton<T extends Object>(T instance) { | ||
return _getIt.registerSingleton<T>(instance); | ||
} | ||
|
||
bool isRegistered<T extends Object>() => _getIt.isRegistered<T>(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// 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 extends Object>(T instance) => registerSingleton<T>(instance); | ||
|
||
@override | ||
T instanceOf<T extends Object>() => instanceFromRegistry<T>(); | ||
|
||
@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) | ||
..viewEngine = _viewEngine; | ||
if (onException != null) pharaoh.onError(onException); | ||
return pharaoh; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import 'package:collection/collection.dart'; | ||
import 'package:reflectable/reflectable.dart' as r; | ||
|
||
import 'container.dart'; | ||
import '../router.dart'; | ||
import '../_router/utils.dart'; | ||
import '../_validation/dto.dart'; | ||
import '../http.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<X> filteredDeclarationsOf<X extends r.DeclarationMirror>( | ||
r.ClassMirror cm, predicate) { | ||
var result = <X>[]; | ||
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<r.VariableMirror> get variables { | ||
return filteredDeclarationsOf(this, (v) => v is r.VariableMirror); | ||
} | ||
|
||
List<r.MethodMirror> get getters { | ||
return filteredDeclarationsOf( | ||
this, (v) => v is r.MethodMirror && v.isGetter); | ||
} | ||
|
||
List<r.MethodMirror> get setters { | ||
return filteredDeclarationsOf( | ||
this, (v) => v is r.MethodMirror && v.isSetter); | ||
} | ||
|
||
List<r.MethodMirror> get methods { | ||
return filteredDeclarationsOf( | ||
this, (v) => v is r.MethodMirror && v.isRegularMethod); | ||
} | ||
} | ||
|
||
T createNewInstance<T extends Object>(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<Map<Symbol, dynamic>>( | ||
{}, (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<r.MethodMirror>(); | ||
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); | ||
}).toList(); | ||
|
||
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<Parent extends Object>(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'); | ||
} | ||
} |
Oops, something went wrong.