From dfbfc4cbcfef2537b5355b2340ba007904d69464 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 12:12:25 -0400 Subject: [PATCH] Dependency Injection! :) --- lib/src/http/angel_base.dart | 5 + lib/src/http/base_middleware.dart | 9 ++ lib/src/http/base_plugin.dart | 8 ++ lib/src/http/controller.dart | 24 +++-- lib/src/http/server.dart | 163 +++++++++++++++++++++--------- pubspec.yaml | 7 +- test/all_tests.dart | 2 + test/di.dart | 87 ++++++++++++++++ 8 files changed, 249 insertions(+), 56 deletions(-) create mode 100644 lib/src/http/base_middleware.dart create mode 100644 lib/src/http/base_plugin.dart create mode 100644 test/di.dart diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart index d95e5cf4..2229c02d 100644 --- a/lib/src/http/angel_base.dart +++ b/lib/src/http/angel_base.dart @@ -1,12 +1,17 @@ library angel_framework.http.angel_base; import 'dart:async'; +import 'package:container/container.dart'; import 'routable.dart'; /// A function that asynchronously generates a view from the given path and data. typedef Future ViewGenerator(String path, [Map data]); class AngelBase extends Routable { + Container _container = new Container(); + /// A [Container] used to inject dependencies. + Container get container => _container; + /// A function that renders views. /// /// Called by [ResponseContext]@`render`. diff --git a/lib/src/http/base_middleware.dart b/lib/src/http/base_middleware.dart new file mode 100644 index 00000000..ca3c5282 --- /dev/null +++ b/lib/src/http/base_middleware.dart @@ -0,0 +1,9 @@ +library angel_framework.http.base_middleware; + +import 'dart:async'; +import 'request_context.dart'; +import 'response_context.dart'; + +abstract class BaseMiddleware { + Future call(RequestContext req, ResponseContext res); +} \ No newline at end of file diff --git a/lib/src/http/base_plugin.dart b/lib/src/http/base_plugin.dart new file mode 100644 index 00000000..ec5ed301 --- /dev/null +++ b/lib/src/http/base_plugin.dart @@ -0,0 +1,8 @@ +library angel_framework.http.base_plugin; + +import 'dart:async'; +import 'server.dart'; + +abstract class AngelPlugin { + Future call(Angel app); +} \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 99d6c0d7..08f94aa1 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -56,16 +56,26 @@ class Controller { args.add(req); else if (parameter.type.reflectedType == ResponseContext) args.add(res); - else { - String name = MirrorSystem.getName(parameter.simpleName); + else {String name = MirrorSystem.getName(parameter.simpleName); var arg = req.params[name]; - if (arg == null && - !exposeMirror.reflectee.allowNull.contain(name)) { - throw new AngelHttpException.BadRequest(); - } + if (arg == null) { + if (parameter.type.reflectedType != dynamic) { + try { + arg = app.container.make(parameter.type.reflectedType); + if (arg != null) { + args.add(arg); + continue; + } + } catch(e) { + // + } + } - args.add(arg); + if (!exposeMirror.reflectee.allowNull.contain(name)) + throw new AngelHttpException.BadRequest(message: "Missing parameter '$name'"); + + } else args.add(arg); } } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 001dee81..6bffaa2a 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -3,6 +3,7 @@ library angel_framework.http.server; import 'dart:async'; import 'dart:io'; import 'dart:math' show Random; +import 'dart:mirrors'; import 'package:json_god/json_god.dart' as god; import 'angel_base.dart'; import 'angel_http_exception.dart'; @@ -12,40 +13,41 @@ import 'response_context.dart'; import 'routable.dart'; import 'route.dart'; import 'service.dart'; +export 'package:container/container.dart'; /// A function that binds an [Angel] server to an Internet address and port. typedef Future ServerGenerator(InternetAddress address, int port); /// Handles an [AngelHttpException]. -typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, - ResponseContext res); +typedef Future AngelErrorHandler( + AngelHttpException err, RequestContext req, ResponseContext res); /// A function that configures an [AngelBase] server in some way. typedef Future AngelConfigurer(AngelBase app); /// A powerful real-time/REST/MVC server class. class Angel extends AngelBase { - var _beforeProcessed = new StreamController(); - var _afterProcessed = new StreamController(); + var _afterProcessed = new StreamController.broadcast(); + var _beforeProcessed = new StreamController.broadcast(); var _onController = new StreamController.broadcast(); + ServerGenerator _serverGenerator = + (address, port) async => await HttpServer.bind(address, port); + + /// Fired after a request is processed. Always runs. + Stream get afterProcessed => _afterProcessed.stream; /// Fired before a request is processed. Always runs. Stream get beforeProcessed => _beforeProcessed.stream; - /// Fired after a request is processed. Always runs. - Stream get afterProcessed => _afterProcessed.stream; /// Fired whenever a controller is added to this instance. /// /// **NOTE**: This is a broadcast stream. Stream get onController => _onController.stream; - ServerGenerator _serverGenerator = - (address, port) async => await HttpServer.bind(address, port); - /// Default error handler, show HTML error page - AngelErrorHandler _errorHandler = (AngelHttpException e, req, - ResponseContext res) { + AngelErrorHandler _errorHandler = + (AngelHttpException e, req, ResponseContext res) { res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.status(e.statusCode); res.write("${e.message}"); @@ -69,9 +71,9 @@ class Angel extends AngelBase { /// Starts the server. /// /// Returns false on failure; otherwise, returns the HttpServer. - startServer(InternetAddress address, int port) async { + Future startServer([InternetAddress address, int port]) async { var server = - await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); + await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0); this.httpServer = server; server.listen(handleRequest); @@ -79,29 +81,42 @@ class Angel extends AngelBase { return server; } + /// Loads some base dependencies into the service container. + void bootstrapContainer() { + container.singleton(this, as: AngelBase); + container.singleton(this); + + if (runtimeType != Angel) + container.singleton(this, as: Angel); + } + Future handleRequest(HttpRequest request) async { _beforeProcessed.add(request); - String req_url = - request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll( - new RegExp(r'\/+$'), ''); - if (req_url.isEmpty) req_url = '/'; + + String requestedUrl = request.uri + .toString() + .replaceAll("?" + request.uri.query, "") + .replaceAll(new RegExp(r'\/+$'), ''); + + if (requestedUrl.isEmpty) requestedUrl = '/'; + RequestContext req = await RequestContext.from(request, {}, this, null); ResponseContext res = await ResponseContext.from(request.response, this); bool canContinue = true; - var execHandler = (handler, req) async { + executeHandler(handler, req) async { if (canContinue) { - canContinue = await new Future.sync(() async { - return _applyHandler(handler, req, res); - }).catchError((e, [StackTrace stackTrace]) async { + try { + canContinue = await _applyHandler(handler, req, res); + } catch (e, stackTrace) { if (e is AngelHttpException) { // Special handling for AngelHttpExceptions :) try { res.status(e.statusCode); String accept = request.headers.value(HttpHeaders.ACCEPT); if (accept == "*/*" || - accept.contains("application/json") || + accept.contains(ContentType.JSON.mimeType) || accept.contains("application/javascript")) { res.json(e.toMap()); } else { @@ -113,38 +128,41 @@ class Angel extends AngelBase { _onError(e, stackTrace); canContinue = false; return false; - }); + } } else return false; - }; + } for (var handler in before) { - await execHandler(handler, req); + await executeHandler(handler, req); } for (Route route in routes) { if (!canContinue) break; - if (route.matcher.hasMatch(req_url) && + + if (route.matcher.hasMatch(requestedUrl) && (request.method == route.method || route.method == '*')) { - req.params = route.parseParameters(req_url); + req.params = route.parseParameters(requestedUrl); req.route = route; for (var handler in route.handlers) { - await execHandler(handler, req); + await executeHandler(handler, req); } } } for (var handler in after) { - await execHandler(handler, req); + await executeHandler(handler, req); } + _finalizeResponse(request, res); } - Future _applyHandler(handler, RequestContext req, - ResponseContext res) async { + Future _applyHandler( + handler, RequestContext req, ResponseContext res) async { if (handler is RequestMiddleware) { var result = await handler(req, res); + if (result is bool) return result == true; else if (result != null) { @@ -157,7 +175,9 @@ class Angel extends AngelBase { if (handler is RequestHandler) { await handler(req, res); return res.isOpen; - } else if (handler is RawRequestHandler) { + } + + if (handler is RawRequestHandler) { var result = await handler(req.underlyingRequest); if (result is bool) return result == true; @@ -166,8 +186,10 @@ class Angel extends AngelBase { return false; } else return true; - } else if (handler is Function || handler is Future) { - var result = await handler(); + } + + if (handler is Future) { + var result = await handler; if (result is bool) return result == true; else if (result != null) { @@ -175,14 +197,27 @@ class Angel extends AngelBase { return false; } else return true; - } else if (requestMiddleware.containsKey(handler)) { - return await _applyHandler(requestMiddleware[handler], req, res); - } else { - res.willCloseItself = true; - res.underlyingResponse.write(god.serialize(handler)); - await res.underlyingResponse.close(); - return false; } + + if (handler is Function) { + var result = await runContained(handler, req, res); + if (result is bool) + return result == true; + else if (result != null) { + res.json(result); + return false; + } else + return true; + } + + if (requestMiddleware.containsKey(handler)) { + return await _applyHandler(requestMiddleware[handler], req, res); + } + + res.willCloseItself = true; + res.underlyingResponse.write(god.serialize(handler)); + await res.underlyingResponse.close(); + return false; } _finalizeResponse(HttpRequest request, ResponseContext res) async { @@ -193,7 +228,7 @@ class Angel extends AngelBase { _afterProcessed.add(request); } } catch (e) { - // Remember: This fails silently + failSilently(request, res); } } @@ -206,14 +241,47 @@ class Angel extends AngelBase { return new String.fromCharCodes(codeUnits); } + // Run a function after injecting from service container + Future runContained(Function handler, RequestContext req, ResponseContext res) async { + ClosureMirror closureMirror = reflect(handler); + List args = []; + + for (ParameterMirror parameter in closureMirror.function.parameters) { + if (parameter.type.reflectedType == RequestContext) + args.add(req); + else if (parameter.type.reflectedType == ResponseContext) + args.add(res); + else { + // First, search to see if we can map this to a type + if (parameter.type.reflectedType != dynamic) { + args.add(container.make(parameter.type.reflectedType)); + } else { + String name = MirrorSystem.getName(parameter.simpleName); + + if (name == "req") + args.add(req); + else if (name == "res") + args.add(res); + else { + throw new Exception("Cannot resolve parameter '$name' within handler."); + } + } + } + } + + return await closureMirror.apply(args).reflectee; + } + /// Applies an [AngelConfigurer] to this instance. Future configure(AngelConfigurer configurer) async { await configurer(this); - if (configurer is Controller) - _onController.add(configurer); + if (configurer is Controller) _onController.add(configurer); } + /// Fallback when an error is thrown while handling a request. + void failSilently(HttpRequest request, ResponseContext res) {} + /// Starts the server. void listen({InternetAddress address, int port: 3000}) { runZoned(() async { @@ -242,7 +310,9 @@ class Angel extends AngelBase { if (stackTrace != null) stderr.write(stackTrace.toString()); } - Angel() : super() {} + Angel() : super() { + bootstrapContainer(); + } /// Creates an HTTPS server. /// Provide paths to a certificate chain and server key (both .pem). @@ -251,9 +321,10 @@ class Angel extends AngelBase { Angel.secure(String certificateChainPath, String serverKeyPath, {String password}) : super() { + bootstrapContainer(); _serverGenerator = (InternetAddress address, int port) async { var certificateChain = - Platform.script.resolve('server_chain.pem').toFilePath(); + Platform.script.resolve('server_chain.pem').toFilePath(); var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); diff --git a/pubspec.yaml b/pubspec.yaml index b8f78b59..de100a9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,14 @@ name: angel_framework -version: 1.0.0-dev.15 +version: 1.0.0-dev.16 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework dependencies: body_parser: ">=1.0.0-dev <2.0.0" + container: ">=0.1.2 <1.0.0" json_god: ">=2.0.0-beta <3.0.0" merge_map: ">=1.0.0 <2.0.0" mime: ">=0.9.3 <1.0.0" dev_dependencies: - http: ">= 0.11.3 < 0.12.0" - test: ">= 0.12.13 < 0.13.0" \ No newline at end of file + http: ">= 0.11.3 <0.12.0" + test: ">= 0.12.13 <0.13.0" \ No newline at end of file diff --git a/test/all_tests.dart b/test/all_tests.dart index 1df0368c..3b289eef 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -1,5 +1,6 @@ import 'package:test/test.dart'; import 'controller.dart' as controller; +import 'di.dart' as di; import 'hooked.dart' as hooked; import 'routing.dart' as routing; import 'services.dart' as services; @@ -7,6 +8,7 @@ import 'services.dart' as services; main() { group('controller', controller.main); group('hooked', hooked.main); + group('di', di.main); group('routing', routing.main); group('services', services.main); } \ No newline at end of file diff --git a/test/di.dart b/test/di.dart new file mode 100644 index 00000000..8e2bdc22 --- /dev/null +++ b/test/di.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; +import 'common.dart'; + +final String TEXT = "make your bed"; +final String OVER = "never"; + +main() { + Angel app; + http.Client client; + HttpServer server; + String url; + + setUp(() async { + app = new Angel(); + client = new http.Client(); + + // Inject some todos + app.container.singleton(new Todo(text: TEXT, over: OVER)); + + app.get("/errands", (Todo singleton) => singleton); + app.get("/errands3", (Errand singleton, Todo foo, RequestContext req) => singleton.text); + await app.configure(new SingletonController()); + await app.configure(new ErrandController()); + + server = await app.startServer(); + url = "http://${server.address.host}:${server.port}"; + }); + + tearDown(() async { + app = null; + url = null; + client.close(); + client = null; + await server.close(force: true); + }); + + test("singleton in route", () async { + validateTodoSingleton(await client.get("$url/errands")); + }); + + test("singleton in controller", () async { + validateTodoSingleton(await client.get("$url/errands2")); + }); + + test("make in route", () async { + var response = await client.get("$url/errands3"); + String text = await JSON.decode(response.body); + expect(text, equals(TEXT)); + }); + + test("make in controller", () async { + var response = await client.get("$url/errands4"); + String text = await JSON.decode(response.body); + expect(text, equals(TEXT)); + }); +} + +void validateTodoSingleton(response) { + Map todo = JSON.decode(response.body); + expect(todo.keys.length, equals(3)); + expect(todo["id"], equals(null)); + expect(todo["text"], equals(TEXT)); + expect(todo["over"], equals(OVER)); +} + +@Expose("/errands2") +class SingletonController extends Controller { + @Expose("/") + todo(Todo singleton) => singleton; +} + +@Expose("/errands4") +class ErrandController extends Controller { + @Expose("/") + errand(Errand errand) => errand.text; +} + +class Errand { + Todo todo; + String get text => todo.text; + + Errand(this.todo); +} \ No newline at end of file