Skip to content

Commit

Permalink
feat: Implement pharaoh framework (#120)
Browse files Browse the repository at this point in the history
* implement pharaoh next

* add tests for pharaoh next

* fix lint issues

* ignore generated files

* run generator

* damn

* :)

* tiny fix
  • Loading branch information
codekeyz authored Mar 5, 2024
1 parent 400cd9e commit 7186055
Show file tree
Hide file tree
Showing 30 changed files with 2,173 additions and 15 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ jobs:
run: melos format -- --set-exit-if-changed

- name: Check linting
run: melos analyze
run: |
cd packages/pharaoh && dart run build_runner build --delete-conflicting-outputs
melos analyze
test:
name: Test Packages
Expand All @@ -47,6 +49,7 @@ jobs:
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
Expand Down
3 changes: 2 additions & 1 deletion packages/pharaoh/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
.idea/
**/pubspec_overrides.yaml
**/pubspec_overrides.yaml
**.reflectable.dart
70 changes: 70 additions & 0 deletions packages/pharaoh/lib/next/_core/config.dart
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;
}
18 changes: 18 additions & 0 deletions packages/pharaoh/lib/next/_core/container.dart
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>();
47 changes: 47 additions & 0 deletions packages/pharaoh/lib/next/_core/core_impl.dart
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;
}
}
157 changes: 157 additions & 0 deletions packages/pharaoh/lib/next/_core/reflector.dart
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');
}
}
Loading

0 comments on commit 7186055

Please sign in to comment.