Dependency Injection! :)

This commit is contained in:
thosakwe 2016-09-17 12:12:25 -04:00
parent 52c5112d93
commit dfbfc4cbcf
8 changed files with 249 additions and 56 deletions

View file

@ -1,12 +1,17 @@
library angel_framework.http.angel_base; library angel_framework.http.angel_base;
import 'dart:async'; import 'dart:async';
import 'package:container/container.dart';
import 'routable.dart'; import 'routable.dart';
/// A function that asynchronously generates a view from the given path and data. /// A function that asynchronously generates a view from the given path and data.
typedef Future<String> ViewGenerator(String path, [Map data]); typedef Future<String> ViewGenerator(String path, [Map data]);
class AngelBase extends Routable { class AngelBase extends Routable {
Container _container = new Container();
/// A [Container] used to inject dependencies.
Container get container => _container;
/// A function that renders views. /// A function that renders views.
/// ///
/// Called by [ResponseContext]@`render`. /// Called by [ResponseContext]@`render`.

View file

@ -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<bool> call(RequestContext req, ResponseContext res);
}

View file

@ -0,0 +1,8 @@
library angel_framework.http.base_plugin;
import 'dart:async';
import 'server.dart';
abstract class AngelPlugin {
Future call(Angel app);
}

View file

@ -56,16 +56,26 @@ class Controller {
args.add(req); args.add(req);
else if (parameter.type.reflectedType == ResponseContext) else if (parameter.type.reflectedType == ResponseContext)
args.add(res); args.add(res);
else { else {String name = MirrorSystem.getName(parameter.simpleName);
String name = MirrorSystem.getName(parameter.simpleName);
var arg = req.params[name]; var arg = req.params[name];
if (arg == null && if (arg == null) {
!exposeMirror.reflectee.allowNull.contain(name)) { if (parameter.type.reflectedType != dynamic) {
throw new AngelHttpException.BadRequest(); 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);
} }
} }

View file

@ -3,6 +3,7 @@ library angel_framework.http.server;
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math' show Random; import 'dart:math' show Random;
import 'dart:mirrors';
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'angel_base.dart'; import 'angel_base.dart';
import 'angel_http_exception.dart'; import 'angel_http_exception.dart';
@ -12,40 +13,41 @@ import 'response_context.dart';
import 'routable.dart'; import 'routable.dart';
import 'route.dart'; import 'route.dart';
import 'service.dart'; import 'service.dart';
export 'package:container/container.dart';
/// A function that binds an [Angel] server to an Internet address and port. /// A function that binds an [Angel] server to an Internet address and port.
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port); typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
/// Handles an [AngelHttpException]. /// Handles an [AngelHttpException].
typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, typedef Future AngelErrorHandler(
ResponseContext res); AngelHttpException err, RequestContext req, ResponseContext res);
/// A function that configures an [AngelBase] server in some way. /// A function that configures an [AngelBase] server in some way.
typedef Future AngelConfigurer(AngelBase app); typedef Future AngelConfigurer(AngelBase app);
/// A powerful real-time/REST/MVC server class. /// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase { class Angel extends AngelBase {
var _beforeProcessed = new StreamController<HttpRequest>(); var _afterProcessed = new StreamController<HttpRequest>.broadcast();
var _afterProcessed = new StreamController<HttpRequest>(); var _beforeProcessed = new StreamController<HttpRequest>.broadcast();
var _onController = new StreamController<Controller>.broadcast(); var _onController = new StreamController<Controller>.broadcast();
ServerGenerator _serverGenerator =
(address, port) async => await HttpServer.bind(address, port);
/// Fired after a request is processed. Always runs.
Stream<HttpRequest> get afterProcessed => _afterProcessed.stream;
/// Fired before a request is processed. Always runs. /// Fired before a request is processed. Always runs.
Stream<HttpRequest> get beforeProcessed => _beforeProcessed.stream; Stream<HttpRequest> get beforeProcessed => _beforeProcessed.stream;
/// Fired after a request is processed. Always runs.
Stream<HttpRequest> get afterProcessed => _afterProcessed.stream;
/// Fired whenever a controller is added to this instance. /// Fired whenever a controller is added to this instance.
/// ///
/// **NOTE**: This is a broadcast stream. /// **NOTE**: This is a broadcast stream.
Stream<Controller> get onController => _onController.stream; Stream<Controller> get onController => _onController.stream;
ServerGenerator _serverGenerator =
(address, port) async => await HttpServer.bind(address, port);
/// Default error handler, show HTML error page /// Default error handler, show HTML error page
AngelErrorHandler _errorHandler = (AngelHttpException e, req, AngelErrorHandler _errorHandler =
ResponseContext res) { (AngelHttpException e, req, ResponseContext res) {
res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString());
res.status(e.statusCode); res.status(e.statusCode);
res.write("<!DOCTYPE html><html><head><title>${e.message}</title>"); res.write("<!DOCTYPE html><html><head><title>${e.message}</title>");
@ -69,9 +71,9 @@ class Angel extends AngelBase {
/// Starts the server. /// Starts the server.
/// ///
/// Returns false on failure; otherwise, returns the HttpServer. /// Returns false on failure; otherwise, returns the HttpServer.
startServer(InternetAddress address, int port) async { Future<HttpServer> startServer([InternetAddress address, int port]) async {
var server = var server =
await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0);
this.httpServer = server; this.httpServer = server;
server.listen(handleRequest); server.listen(handleRequest);
@ -79,29 +81,42 @@ class Angel extends AngelBase {
return server; 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 { Future handleRequest(HttpRequest request) async {
_beforeProcessed.add(request); _beforeProcessed.add(request);
String req_url =
request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll( String requestedUrl = request.uri
new RegExp(r'\/+$'), ''); .toString()
if (req_url.isEmpty) req_url = '/'; .replaceAll("?" + request.uri.query, "")
.replaceAll(new RegExp(r'\/+$'), '');
if (requestedUrl.isEmpty) requestedUrl = '/';
RequestContext req = await RequestContext.from(request, {}, this, null); RequestContext req = await RequestContext.from(request, {}, this, null);
ResponseContext res = await ResponseContext.from(request.response, this); ResponseContext res = await ResponseContext.from(request.response, this);
bool canContinue = true; bool canContinue = true;
var execHandler = (handler, req) async { executeHandler(handler, req) async {
if (canContinue) { if (canContinue) {
canContinue = await new Future.sync(() async { try {
return _applyHandler(handler, req, res); canContinue = await _applyHandler(handler, req, res);
}).catchError((e, [StackTrace stackTrace]) async { } catch (e, stackTrace) {
if (e is AngelHttpException) { if (e is AngelHttpException) {
// Special handling for AngelHttpExceptions :) // Special handling for AngelHttpExceptions :)
try { try {
res.status(e.statusCode); res.status(e.statusCode);
String accept = request.headers.value(HttpHeaders.ACCEPT); String accept = request.headers.value(HttpHeaders.ACCEPT);
if (accept == "*/*" || if (accept == "*/*" ||
accept.contains("application/json") || accept.contains(ContentType.JSON.mimeType) ||
accept.contains("application/javascript")) { accept.contains("application/javascript")) {
res.json(e.toMap()); res.json(e.toMap());
} else { } else {
@ -113,38 +128,41 @@ class Angel extends AngelBase {
_onError(e, stackTrace); _onError(e, stackTrace);
canContinue = false; canContinue = false;
return false; return false;
}); }
} else } else
return false; return false;
}; }
for (var handler in before) { for (var handler in before) {
await execHandler(handler, req); await executeHandler(handler, req);
} }
for (Route route in routes) { for (Route route in routes) {
if (!canContinue) break; if (!canContinue) break;
if (route.matcher.hasMatch(req_url) &&
if (route.matcher.hasMatch(requestedUrl) &&
(request.method == route.method || route.method == '*')) { (request.method == route.method || route.method == '*')) {
req.params = route.parseParameters(req_url); req.params = route.parseParameters(requestedUrl);
req.route = route; req.route = route;
for (var handler in route.handlers) { for (var handler in route.handlers) {
await execHandler(handler, req); await executeHandler(handler, req);
} }
} }
} }
for (var handler in after) { for (var handler in after) {
await execHandler(handler, req); await executeHandler(handler, req);
} }
_finalizeResponse(request, res); _finalizeResponse(request, res);
} }
Future<bool> _applyHandler(handler, RequestContext req, Future<bool> _applyHandler(
ResponseContext res) async { handler, RequestContext req, ResponseContext res) async {
if (handler is RequestMiddleware) { if (handler is RequestMiddleware) {
var result = await handler(req, res); var result = await handler(req, res);
if (result is bool) if (result is bool)
return result == true; return result == true;
else if (result != null) { else if (result != null) {
@ -157,7 +175,9 @@ class Angel extends AngelBase {
if (handler is RequestHandler) { if (handler is RequestHandler) {
await handler(req, res); await handler(req, res);
return res.isOpen; return res.isOpen;
} else if (handler is RawRequestHandler) { }
if (handler is RawRequestHandler) {
var result = await handler(req.underlyingRequest); var result = await handler(req.underlyingRequest);
if (result is bool) if (result is bool)
return result == true; return result == true;
@ -166,8 +186,10 @@ class Angel extends AngelBase {
return false; return false;
} else } else
return true; 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) if (result is bool)
return result == true; return result == true;
else if (result != null) { else if (result != null) {
@ -175,15 +197,28 @@ class Angel extends AngelBase {
return false; return false;
} else } else
return true; return true;
} else if (requestMiddleware.containsKey(handler)) { }
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); return await _applyHandler(requestMiddleware[handler], req, res);
} else { }
res.willCloseItself = true; res.willCloseItself = true;
res.underlyingResponse.write(god.serialize(handler)); res.underlyingResponse.write(god.serialize(handler));
await res.underlyingResponse.close(); await res.underlyingResponse.close();
return false; return false;
} }
}
_finalizeResponse(HttpRequest request, ResponseContext res) async { _finalizeResponse(HttpRequest request, ResponseContext res) async {
try { try {
@ -193,7 +228,7 @@ class Angel extends AngelBase {
_afterProcessed.add(request); _afterProcessed.add(request);
} }
} catch (e) { } catch (e) {
// Remember: This fails silently failSilently(request, res);
} }
} }
@ -206,14 +241,47 @@ class Angel extends AngelBase {
return new String.fromCharCodes(codeUnits); 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. /// Applies an [AngelConfigurer] to this instance.
Future configure(AngelConfigurer configurer) async { Future configure(AngelConfigurer configurer) async {
await configurer(this); await configurer(this);
if (configurer is Controller) if (configurer is Controller) _onController.add(configurer);
_onController.add(configurer);
} }
/// Fallback when an error is thrown while handling a request.
void failSilently(HttpRequest request, ResponseContext res) {}
/// Starts the server. /// Starts the server.
void listen({InternetAddress address, int port: 3000}) { void listen({InternetAddress address, int port: 3000}) {
runZoned(() async { runZoned(() async {
@ -242,7 +310,9 @@ class Angel extends AngelBase {
if (stackTrace != null) stderr.write(stackTrace.toString()); if (stackTrace != null) stderr.write(stackTrace.toString());
} }
Angel() : super() {} Angel() : super() {
bootstrapContainer();
}
/// Creates an HTTPS server. /// Creates an HTTPS server.
/// Provide paths to a certificate chain and server key (both .pem). /// Provide paths to a certificate chain and server key (both .pem).
@ -251,6 +321,7 @@ class Angel extends AngelBase {
Angel.secure(String certificateChainPath, String serverKeyPath, Angel.secure(String certificateChainPath, String serverKeyPath,
{String password}) {String password})
: super() { : super() {
bootstrapContainer();
_serverGenerator = (InternetAddress address, int port) async { _serverGenerator = (InternetAddress address, int port) async {
var certificateChain = var certificateChain =
Platform.script.resolve('server_chain.pem').toFilePath(); Platform.script.resolve('server_chain.pem').toFilePath();

View file

@ -1,10 +1,11 @@
name: angel_framework name: angel_framework
version: 1.0.0-dev.15 version: 1.0.0-dev.16
description: Core libraries for the Angel framework. description: Core libraries for the Angel framework.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_framework homepage: https://github.com/angel-dart/angel_framework
dependencies: dependencies:
body_parser: ">=1.0.0-dev <2.0.0" 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" json_god: ">=2.0.0-beta <3.0.0"
merge_map: ">=1.0.0 <2.0.0" merge_map: ">=1.0.0 <2.0.0"
mime: ">=0.9.3 <1.0.0" mime: ">=0.9.3 <1.0.0"

View file

@ -1,5 +1,6 @@
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'controller.dart' as controller; import 'controller.dart' as controller;
import 'di.dart' as di;
import 'hooked.dart' as hooked; import 'hooked.dart' as hooked;
import 'routing.dart' as routing; import 'routing.dart' as routing;
import 'services.dart' as services; import 'services.dart' as services;
@ -7,6 +8,7 @@ import 'services.dart' as services;
main() { main() {
group('controller', controller.main); group('controller', controller.main);
group('hooked', hooked.main); group('hooked', hooked.main);
group('di', di.main);
group('routing', routing.main); group('routing', routing.main);
group('services', services.main); group('services', services.main);
} }

87
test/di.dart Normal file
View file

@ -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);
}