Dependency Injection! :)
This commit is contained in:
parent
52c5112d93
commit
dfbfc4cbcf
8 changed files with 249 additions and 56 deletions
|
@ -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<String> 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`.
|
||||
|
|
9
lib/src/http/base_middleware.dart
Normal file
9
lib/src/http/base_middleware.dart
Normal 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);
|
||||
}
|
8
lib/src/http/base_plugin.dart
Normal file
8
lib/src/http/base_plugin.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
library angel_framework.http.base_plugin;
|
||||
|
||||
import 'dart:async';
|
||||
import 'server.dart';
|
||||
|
||||
abstract class AngelPlugin {
|
||||
Future call(Angel app);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<HttpServer> 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<HttpRequest>();
|
||||
var _afterProcessed = new StreamController<HttpRequest>();
|
||||
var _afterProcessed = new StreamController<HttpRequest>.broadcast();
|
||||
var _beforeProcessed = new StreamController<HttpRequest>.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.
|
||||
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.
|
||||
///
|
||||
/// **NOTE**: This is a broadcast stream.
|
||||
Stream<Controller> 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("<!DOCTYPE html><html><head><title>${e.message}</title>");
|
||||
|
@ -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<HttpServer> 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<bool> _applyHandler(handler, RequestContext req,
|
||||
ResponseContext res) async {
|
||||
Future<bool> _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,15 +197,28 @@ class Angel extends AngelBase {
|
|||
return false;
|
||||
} else
|
||||
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);
|
||||
} else {
|
||||
}
|
||||
|
||||
res.willCloseItself = true;
|
||||
res.underlyingResponse.write(god.serialize(handler));
|
||||
await res.underlyingResponse.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_finalizeResponse(HttpRequest request, ResponseContext res) async {
|
||||
try {
|
||||
|
@ -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,6 +321,7 @@ 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();
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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 <thosakwe@gmail.com>
|
||||
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"
|
||||
|
|
|
@ -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);
|
||||
}
|
87
test/di.dart
Normal file
87
test/di.dart
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue