2016-09-15 19:53:01 +00:00
|
|
|
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';
|
2016-09-15 19:53:01 +00:00
|
|
|
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;
|
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
/// The [Angel] application powering this controller.
|
2016-12-31 01:46:41 +00:00
|
|
|
Angel get app => _app;
|
2017-03-28 23:29:22 +00:00
|
|
|
|
2016-12-31 01:46:41 +00:00
|
|
|
final bool debug;
|
2017-03-28 23:29:22 +00:00
|
|
|
|
|
|
|
/// If `true` (default), this class will inject itself as a singleton into the [app]'s container when bootstrapped.
|
|
|
|
final bool injectSingleton;
|
|
|
|
|
|
|
|
/// Middleware to run before all handlers in this class.
|
2016-06-27 00:20:42 +00:00
|
|
|
List middleware = [];
|
2017-03-28 23:29:22 +00:00
|
|
|
|
|
|
|
/// A mapping of route paths to routes, produced from the [Expose] annotations on this class.
|
2016-09-15 19:53:01 +00:00
|
|
|
Map<String, Route> routeMappings = {};
|
2016-06-27 00:20:42 +00:00
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
Controller({this.debug: false, this.injectSingleton: true});
|
2016-12-31 01:46:41 +00:00
|
|
|
|
|
|
|
Future call(Angel app) async {
|
2017-03-28 23:29:22 +00:00
|
|
|
_app = app;
|
|
|
|
|
|
|
|
if (injectSingleton != false) _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-06-06 12:42:33 +00:00
|
|
|
var propFromApp;
|
|
|
|
|
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-06-06 12:42:33 +00:00
|
|
|
else if ((propFromApp = req.app.findProperty(requirement)) != null)
|
|
|
|
args.add(propFromApp);
|
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;
|
|
|
|
|
2017-06-06 12:42:33 +00:00
|
|
|
if (req.params.containsKey(key) ||
|
|
|
|
req.injections.containsKey(key) ||
|
|
|
|
req.properties.containsKey(key) ||
|
|
|
|
req.app.properties.containsKey(key)) {
|
2017-01-12 01:52:06 +00:00
|
|
|
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
|
|
|
}
|