platform/lib/src/http/controller.dart

205 lines
6.8 KiB
Dart
Raw Normal View History

library angel_framework.http.controller;
import 'dart:async';
import 'dart:mirrors';
2016-10-22 20:41:36 +00:00
import 'package:angel_route/angel_route.dart';
import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
2016-12-31 02:00:52 +00:00
import 'server.dart' show Angel, preInject;
2016-12-31 01:46:41 +00:00
/// Contains a list of the data required for a DI-enabled method to run.
///
/// This improves performance by removing the necessity to reflect a method
/// every time it is requested.
///
/// Regular request handlers can also skip DI entirely, lowering response time
/// and memory use.
class InjectionRequest {
2017-01-20 22:11:20 +00:00
/// Optional, typed data that can be passed to a DI-enabled method.
Map<String, Type> named = {};
/// A list of the arguments required for a DI-enabled method to run.
2016-12-31 01:46:41 +00:00
final List required = [];
2017-01-15 19:52:14 +00:00
2017-01-20 22:11:20 +00:00
/// A list of the arguments that can be null in a DI-enabled method.
final List<String> optional = [];
2016-12-31 01:46:41 +00:00
}
2016-06-27 00:20:42 +00:00
2016-12-31 01:46:41 +00:00
/// Supports grouping routes with shared functionality.
2016-06-27 00:20:42 +00:00
class Controller {
2016-12-31 01:46:41 +00:00
Angel _app;
Angel get app => _app;
final bool debug;
2016-06-27 00:20:42 +00:00
List middleware = [];
Map<String, Route> routeMappings = {};
2016-06-27 00:20:42 +00:00
2016-12-31 01:46:41 +00:00
Controller({this.debug: false});
Future call(Angel app) async {
2017-02-25 00:16:31 +00:00
_app = app..container.singleton(this);
2016-10-22 20:41:36 +00:00
2016-06-27 00:20:42 +00:00
// Load global expose decl
ClassMirror classMirror = reflectClass(this.runtimeType);
2016-12-31 02:00:52 +00:00
Expose exposeDecl = findExpose();
2016-06-27 00:20:42 +00:00
2016-10-22 20:41:36 +00:00
if (exposeDecl == null) {
2016-06-27 00:20:42 +00:00
throw new Exception(
"All controllers must carry an @Expose() declaration.");
2016-10-22 20:41:36 +00:00
}
2016-12-31 01:46:41 +00:00
var routable = new Routable(debug: debug);
2016-11-28 21:48:00 +00:00
app.use(exposeDecl.path, routable);
2016-11-23 09:10:47 +00:00
TypeMirror typeMirror = reflectType(this.runtimeType);
2016-12-31 01:46:41 +00:00
String name = exposeDecl.as?.isNotEmpty == true
? exposeDecl.as
: MirrorSystem.getName(typeMirror.simpleName);
2016-11-23 09:10:47 +00:00
app.controllers[name] = this;
2016-10-22 20:41:36 +00:00
2016-12-31 01:46:41 +00:00
// Pre-reflect methods
InstanceMirror instanceMirror = reflect(this);
final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware);
final routeBuilder = _routeBuilder(instanceMirror, routable, handlers);
classMirror.instanceMembers.forEach(routeBuilder);
configureRoutes(routable);
}
2016-11-23 09:10:47 +00:00
2016-12-31 01:46:41 +00:00
Function _routeBuilder(
InstanceMirror instanceMirror, Routable routable, List handlers) {
return (Symbol methodName, MethodMirror method) {
if (method.isRegularMethod &&
methodName != #toString &&
methodName != #noSuchMethod &&
methodName != #call &&
methodName != #equals &&
methodName != #==) {
Expose exposeDecl = method.metadata
.map((m) => m.reflectee)
.firstWhere((r) => r is Expose, orElse: () => null);
if (exposeDecl == null) return;
var reflectedMethod = instanceMirror.getField(methodName).reflectee;
var middleware = []..addAll(handlers)..addAll(exposeDecl.middleware);
2017-01-12 01:52:06 +00:00
String name = exposeDecl.as?.isNotEmpty == true
? exposeDecl.as
: MirrorSystem.getName(methodName);
2016-12-31 01:46:41 +00:00
// Check if normal
if (method.parameters.length == 2 &&
method.parameters[0].type.reflectedType == RequestContext &&
method.parameters[1].type.reflectedType == ResponseContext) {
// Create a regular route
2017-01-12 01:52:06 +00:00
routeMappings[name] = routable
.addRoute(exposeDecl.method, exposeDecl.path, (req, res) async {
var result = await reflectedMethod(req, res);
return result is RequestHandler ? await result(req, res) : result;
}, middleware: middleware);
2016-12-31 01:46:41 +00:00
return;
}
2016-06-27 00:20:42 +00:00
2017-01-15 19:52:14 +00:00
var injection = preInject(reflectedMethod);
if (exposeDecl?.allowNull?.isNotEmpty == true)
injection.optional?.addAll(exposeDecl.allowNull);
routeMappings[name] = routable.addRoute(exposeDecl.method,
exposeDecl.path, handleContained(reflectedMethod, injection),
2016-12-31 01:46:41 +00:00
middleware: middleware);
2016-06-27 00:20:42 +00:00
}
2016-11-23 09:10:47 +00:00
};
}
2016-12-31 01:46:41 +00:00
/// Used to add additional routes to the router from within a [Controller].
void configureRoutes(Routable routable) {}
2016-12-31 02:00:52 +00:00
/// Finds the [Expose] declaration for this class.
Expose findExpose() => reflectClass(runtimeType)
.metadata
.map((m) => m.reflectee)
.firstWhere((r) => r is Expose, orElse: () => null);
2016-12-31 01:46:41 +00:00
}
2017-01-15 19:52:14 +00:00
/// Shortcut for calling [preInject], and then [handleContained].
2017-01-20 22:11:20 +00:00
///
2017-01-15 19:52:14 +00:00
/// Use this to instantly create a request handler for a DI-enabled method.
RequestHandler createDynamicHandler(handler,
{Iterable<String> optional: const []}) {
var injection = preInject(handler);
injection.optional.addAll(optional ?? []);
return handleContained(handler, injection);
}
2016-12-31 01:46:41 +00:00
/// Handles a request with a DI-enabled handler.
RequestHandler handleContained(handler, InjectionRequest injection) {
return (RequestContext req, ResponseContext res) async {
List args = [];
void inject(requirement) {
2017-01-12 01:52:06 +00:00
if (requirement == RequestContext) {
args.add(req);
} else if (requirement == ResponseContext) {
args.add(res);
} else if (requirement is String) {
if (req.params.containsKey(requirement)) {
2016-12-31 01:46:41 +00:00
args.add(req.params[requirement]);
2017-01-12 01:52:06 +00:00
} else if (req.injections.containsKey(requirement))
args.add(req.injections[requirement]);
2017-02-26 21:31:09 +00:00
else if (req.properties.containsKey(requirement))
args.add(req.properties[requirement]);
2017-01-15 19:52:14 +00:00
else if (injection.optional.contains(requirement))
args.add(null);
2017-01-12 01:52:06 +00:00
else {
2016-12-31 01:46:41 +00:00
throw new ArgumentError(
2017-01-12 01:52:06 +00:00
"Cannot resolve parameter '$requirement' within handler.");
2016-12-31 01:46:41 +00:00
}
2017-01-12 01:52:06 +00:00
} else if (requirement is List &&
requirement.length == 2 &&
requirement.first is String &&
requirement.last is Type) {
String key = requirement.first;
Type type = requirement.last;
if (req.params.containsKey(key) || req.injections.containsKey(key)) {
inject(key);
} else
inject(type);
} else if (requirement is Type && requirement != dynamic) {
if (req.injections.containsKey(requirement))
args.add(req.injections[requirement]);
else
args.add(req.app.container.make(requirement));
} else {
throw new ArgumentError(
'$requirement cannot be injected into a request handler.');
2016-12-31 01:46:41 +00:00
}
}
2017-01-20 22:11:20 +00:00
Map<Symbol, dynamic> named = {};
2016-12-31 01:46:41 +00:00
injection.required.forEach(inject);
2017-01-20 22:11:20 +00:00
injection.named.forEach((k, v) {
var name = new Symbol(k);
if (req.params.containsKey(k))
named[name] = v;
else if (req.injections.containsKey(k))
named[name] = v;
else if (req.injections.containsKey(v) && v != dynamic)
named[name] = v;
else {
try {
named[name] = req.app.container.make(v);
} catch (e) {
named[name] = null;
}
}
});
var result = Function.apply(handler, args, named);
2017-01-12 01:52:06 +00:00
return result is Future ? await result : result;
2016-12-31 01:46:41 +00:00
};
2016-10-22 20:41:36 +00:00
}