diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/README.md b/README.md index 0e96b466..bb83bbe0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # angel_framework -![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev-red.svg) +![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev.23-red.svg) +![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index f48516c5..7eb716ba 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -18,19 +18,6 @@ class Controller { Future call(AngelBase app) async { this.app = app; - app.use(exposeDecl.path, generateRoutable()); - - TypeMirror typeMirror = reflectType(this.runtimeType); - String name = exposeDecl.as; - - if (name == null || name.isEmpty) - name = MirrorSystem.getName(typeMirror.simpleName); - - app.controllers[name] = this; - } - - Routable generateRoutable() { - final routable = new Routable(); // Load global expose decl ClassMirror classMirror = reflectClass(this.runtimeType); @@ -47,11 +34,18 @@ class Controller { "All controllers must carry an @Expose() declaration."); } - final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware); + app.use(exposeDecl.path, generateRoutable(classMirror)); + TypeMirror typeMirror = reflectType(this.runtimeType); + String name = exposeDecl.as; - InstanceMirror instanceMirror = reflect(this); - classMirror.instanceMembers - .forEach((Symbol key, MethodMirror methodMirror) { + if (name == null || name.isEmpty) + name = MirrorSystem.getName(typeMirror.simpleName); + + app.controllers[name] = this; + } + + _callback(InstanceMirror instanceMirror, Routable routable, List handlers) { + return (Symbol key, MethodMirror methodMirror) { if (methodMirror.isRegularMethod && key != #toString && key != #noSuchMethod && @@ -102,11 +96,13 @@ class Controller { return await instanceMirror.invoke(key, args).reflectee; }; + final middleware = [] + ..addAll(handlers) + ..addAll(exposeMirror.reflectee.middleware); + final route = routable.addRoute(exposeMirror.reflectee.method, exposeMirror.reflectee.path, handler, - middleware: [] - ..addAll(handlers) - ..addAll(exposeMirror.reflectee.middleware)); + middleware: middleware); String name = exposeMirror.reflectee.as; @@ -115,7 +111,16 @@ class Controller { routeMappings[name] = route; } } - }); + }; + } + + Routable generateRoutable(ClassMirror classMirror) { + final routable = new Routable(debug: true); + final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware); + + InstanceMirror instanceMirror = reflect(this); + final callback = _callback(instanceMirror, routable, handlers); + classMirror.instanceMembers.forEach(callback); return routable; } diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart index 546e2e52..3f7552f8 100644 --- a/lib/src/http/hooked_service.dart +++ b/lib/src/http/hooked_service.dart @@ -37,7 +37,10 @@ class HookedService extends Service { HookedService(Service this.inner) { // Clone app instance if (inner.app != null) this.app = inner.app; + } + @override + void addRoutes() { // Set up our routes. We still need to copy middleware from inner service Map restProvider = {'provider': Providers.REST}; @@ -45,7 +48,7 @@ class HookedService extends Service { Middleware before = getAnnotation(inner, Middleware); final handlers = []; - if (before != null) handlers.add(before.handlers); + if (before != null) handlers.addAll(before.handlers); Middleware indexMiddleware = getAnnotation(inner.index, Middleware); get('/', (req, res) async { @@ -67,7 +70,7 @@ class HookedService extends Service { get( '/:id', (req, res) async => await this - .read(req.params['id'], mergeMap([req.query, restProvider])), + .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); @@ -76,7 +79,7 @@ class HookedService extends Service { patch( '/:id', (req, res) async => - await this.modify(req.params['id'], req.body, restProvider), + await this.modify(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -86,7 +89,7 @@ class HookedService extends Service { post( '/:id', (req, res) async => - await this.update(req.params['id'], req.body, restProvider), + await this.update(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -96,7 +99,7 @@ class HookedService extends Service { delete( '/:id', (req, res) async => await this - .remove(req.params['id'], mergeMap([req.query, restProvider])), + .remove(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 84ffedb2..4e9c281b 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -109,8 +109,19 @@ class ResponseContext extends Extensible { /// Redirects to the given named [Route]. void redirectTo(String name, [Map params, int code]) { - // Todo: Need to recurse route hierarchy, but also efficiently :) - Route matched = app.routes.firstWhere((Route route) => route.name == name); + _findRoute(Route route) { + for (Route child in route.children) { + final resolved = _findRoute(child); + + if (resolved != null) return resolved; + } + + return route.children + .firstWhere((r) => r.name == name, orElse: () => null); + } + + Route matched = _findRoute(app.root); + if (matched != null) { redirect(matched.makeUri(params), code: code); return; @@ -160,7 +171,8 @@ class ResponseContext extends Extensible { if (isOpen) { if (value is List) buffer.add(value); - else buffer.add(encoding.encode(value.toString())); + else + buffer.add(encoding.encode(value.toString())); } } } diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index ae1d844d..7a25fc7f 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -33,12 +33,10 @@ class Routable extends Router { final Map requestMiddleware = {}; /// A set of [Service] objects that have been mapped into routes. - Map get services => - new Map.unmodifiable(_services); + Map get services => _services; /// A set of [Controller] objects that have been loaded into the application. - Map get controllers => - new Map.unmodifiable(_controllers); + Map get controllers => _controllers; StreamController _onService = new StreamController.broadcast(); @@ -61,7 +59,7 @@ class Routable extends Router { @override Route addRoute(String method, Pattern path, Object handler, - {List middleware}) { + {List middleware: const []}) { final List handlers = []; // Merge @Middleware declaration, if any Middleware middlewareDeclaration = getAnnotation(handler, Middleware); @@ -69,8 +67,11 @@ class Routable extends Router { handlers.addAll(middlewareDeclaration.handlers); } - return super.addRoute(method, path, handler, - middleware: []..addAll(middleware ?? [])..addAll(handlers)); + final List handlerSequence = []; + handlerSequence.addAll(middleware ?? []); + handlerSequence.addAll(handlers); + + return super.addRoute(method, path, handler, middleware: handlerSequence); } void use(Pattern path, Router router, @@ -89,9 +90,11 @@ class Routable extends Router { .toString() .trim() .replaceAll(new RegExp(r'(^/+)|(/+$)'), '')] = service; + service.addRoutes(); } final handlers = []; + if (_router is AngelBase) { handlers.add((RequestContext req, ResponseContext res) async { req.app = _router; @@ -109,9 +112,9 @@ class Routable extends Router { copiedMiddleware[middlewareName]; } - root.child(path, debug: debug, handlers: handlers).addChild(router.root); - - _router.dumpTree(header: 'Mounting on "$path":'); + // _router.dumpTree(header: 'Mounting on "$path":'); + // root.child(path, debug: debug, handlers: handlers).addChild(router.root); + mount(path, _router); if (router is Routable) { // Copy services, too. :) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 420fa01a..72c77ed4 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -98,8 +98,8 @@ class Angel extends AngelBase { /// Returns false on failure; otherwise, returns the HttpServer. Future startServer([InternetAddress address, int port]) async { final host = address ?? InternetAddress.LOOPBACK_IP_V4; - final server = await _serverGenerator(host, port ?? 0); - return this.httpServer = server..listen(handleRequest); + this.httpServer = await _serverGenerator(host, port ?? 0); + return httpServer..listen(handleRequest); } /// Loads some base dependencies into the service container. @@ -178,14 +178,13 @@ class Angel extends AngelBase { final req = await RequestContext.from(request, this); final res = new ResponseContext(request.response, this); String requestedUrl = request.uri - .toString() - .replaceAll("?" + request.uri.query, "") + .path .replaceAll(_straySlashes, ''); if (requestedUrl.isEmpty) requestedUrl = '/'; final route = resolve(requestedUrl, method: request.method); - print('Resolve ${requestedUrl} -> $route'); + _printDebug('Resolved ${requestedUrl} -> $route'); req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); final handlerSequence = []..addAll(before); @@ -331,8 +330,8 @@ class Angel extends AngelBase { bootstrapContainer(); _serverGenerator = (InternetAddress address, int port) async { var certificateChain = - Platform.script.resolve('server_chain.pem').toFilePath(); - var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); + Platform.script.resolve(certificateChainPath).toFilePath(); + var serverKey = Platform.script.resolve(serverKeyPath).toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); serverContext.usePrivateKey(serverKey, diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index b04ddd4a..723f7ae6 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -65,14 +65,14 @@ class Service extends Routable { throw new AngelHttpException.MethodNotAllowed(); } - Service() : super() { + void addRoutes() { Map restProvider = {'provider': Providers.REST}; // Add global middleware if declared on the instance itself Middleware before = getAnnotation(this, Middleware); final handlers = []; - if (before != null) handlers.add(before.handlers); + if (before != null) handlers.addAll(before.handlers); Middleware indexMiddleware = getAnnotation(this.index, Middleware); get('/', (req, res) async { @@ -94,7 +94,7 @@ class Service extends Routable { get( '/:id', (req, res) async => await this - .read(req.params['id'], mergeMap([req.query, restProvider])), + .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); @@ -103,7 +103,7 @@ class Service extends Routable { patch( '/:id', (req, res) async => - await this.modify(req.params['id'], req.body, restProvider), + await this.modify(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -113,7 +113,7 @@ class Service extends Routable { post( '/:id', (req, res) async => - await this.update(req.params['id'], req.body, restProvider), + await this.update(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -123,10 +123,12 @@ class Service extends Routable { delete( '/:id', (req, res) async => await this - .remove(req.params['id'], mergeMap([req.query, restProvider])), + .remove(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( (removeMiddleware == null) ? [] : removeMiddleware.handlers)); + + normalize(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 0d1e5031..d5703a9d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,12 @@ name: angel_framework -version: 1.0.0-dev.22 +version: 1.0.0-dev.23 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework environment: sdk: ">=1.18.0" dependencies: - angel_route: - path: ../angel_route + angel_route: ^1.0.0-dev body_parser: ^1.0.0-dev container: ^0.1.2 json_god: ^2.0.0-beta diff --git a/test/controller.dart b/test/controller.dart index 8af37c83..2fb6e7bc 100644 --- a/test/controller.dart +++ b/test/controller.dart @@ -10,23 +10,23 @@ import 'common.dart'; class TodoController extends Controller { List todos = [new Todo(text: "Hello", over: "world")]; - @Expose("/:id", middleware: const["bar"]) - Future fetchTodo(int id, RequestContext req, - ResponseContext res) async { + @Expose("/:id", middleware: const ["bar"]) + Future fetchTodo( + int id, RequestContext req, ResponseContext res) async { expect(req, isNotNull); expect(res, isNotNull); return todos[id]; } @Expose("/namedRoute/:foo", as: "foo") - Future someRandomRoute(RequestContext req, - ResponseContext res) async { + Future someRandomRoute( + RequestContext req, ResponseContext res) async { return "${req.params['foo']}!"; } } main() { - Angel app = new Angel(); + Angel app; HttpServer server; InternetAddress host = InternetAddress.LOOPBACK_IP_V4; int port = 3000; @@ -34,25 +34,25 @@ main() { String url = "http://${host.address}:$port"; setUp(() async { + app = new Angel(); app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); app.registerMiddleware("bar", (req, res) async => res.write("world!")); - app.get("/redirect", (req, ResponseContext res) async => - res.redirectToAction("TodoController@foo", {"foo": "world"})); + app.get( + "/redirect", + (req, ResponseContext res) async => + res.redirectToAction("TodoController@foo", {"foo": "world"})); await app.configure(new TodoController()); print(app.controllers); - print("\nDUMPING ROUTES:"); - app.routes.forEach((Route route) { - print("\t${route.method} ${route.path} -> ${route.handlers}"); - }); - print("\n"); + app.dumpTree(); server = await app.startServer(host, port); client = new http.Client(); }); tearDown(() async { - await (server ?? app.httpServer).close(force: true); + await server.close(force: true); + app = null; client.close(); client = null; }); @@ -62,7 +62,7 @@ main() { var response = await client.get("$url/todos/0"); print(response.body); - expect(response.body.indexOf("Hello, "), equals(0)); + expect(rgx.firstMatch(response.body).start, equals(0)); Map todo = JSON.decode(response.body.replaceAll(rgx, "")); print("Todo: $todo"); diff --git a/test/di.dart b/test/di.dart index 8e2bdc22..bfc9d9af 100644 --- a/test/di.dart +++ b/test/di.dart @@ -15,7 +15,7 @@ main() { String url; setUp(() async { - app = new Angel(); + app = new Angel(debug: true); client = new http.Client(); // Inject some todos @@ -27,6 +27,7 @@ main() { await app.configure(new ErrandController()); server = await app.startServer(); + print('server: $server, httpServer: ${app.httpServer}'); url = "http://${server.address.host}:${server.port}"; }); diff --git a/test/hooked.dart b/test/hooked.dart index b72d7930..f9026d36 100644 --- a/test/hooked.dart +++ b/test/hooked.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; @@ -6,74 +7,86 @@ import 'common.dart'; main() { Map headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }; - Angel app; - String url; - http.Client client; - HookedService Todos; + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; - setUp(() async { - app = new Angel(); - client = new http.Client(); - app.use('/todos', new MemoryService()); - Todos = app.service("todos"); + Angel app; + HttpServer server; + String url; + http.Client client; + HookedService Todos; - await app.startServer(null, 0); - url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; - }); + setUp(() async { + app = new Angel(debug: true); + client = new http.Client(); + app.use('/todos', new MemoryService()); + Todos = app.service("todos"); - tearDown(() async { - app = null; - url = null; - client.close(); - client = null; - Todos = null; - }); + app + ..normalize() + ..dumpTree(showMatchers: true); - test("listen before and after", () async { - int count = 0; + server = await app.startServer(); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); - Todos - ..beforeIndexed.listen((_) { - count++; - }) - ..afterIndexed.listen((_) { - count++; - }); + tearDown(() async { + await server.close(force: true); + app = null; + url = null; + client.close(); + client = null; + Todos = null; + }); - var response = await client.get("$url/todos"); - print(response.body); - expect(count, equals(2)); - }); + test("listen before and after", () async { + int count = 0; - test("cancel before", () async { - Todos.beforeCreated..listen((HookedServiceEvent event) { + Todos + ..beforeIndexed.listen((_) { + count++; + }) + ..afterIndexed.listen((_) { + count++; + }); + + var response = await client.get("$url/todos"); + print(response.body); + expect(count, equals(2)); + }); + + test("cancel before", () async { + Todos.beforeCreated + ..listen((HookedServiceEvent event) { event.cancel({"hello": "hooked world"}); - })..listen((HookedServiceEvent event) { + }) + ..listen((HookedServiceEvent event) { event.cancel({"this_hook": "should never run"}); }); - var response = await client.post( - "$url/todos", body: god.serialize({"arbitrary": "data"}), - headers: headers); - print(response.body); - Map result = god.deserialize(response.body); - expect(result["hello"], equals("hooked world")); - }); + var response = await client.post("$url/todos", + body: god.serialize({"arbitrary": "data"}), headers: headers); + print(response.body); + Map result = god.deserialize(response.body); + expect(result["hello"], equals("hooked world")); + }); - test("cancel after", () async { - Todos.afterIndexed..listen((HookedServiceEvent event) async { + test("cancel after", () async { + Todos.afterIndexed + ..listen((HookedServiceEvent event) async { // Hooks can be Futures ;) - event.cancel([{"angel": "framework"}]); - })..listen((HookedServiceEvent event) { + event.cancel([ + {"angel": "framework"} + ]); + }) + ..listen((HookedServiceEvent event) { event.cancel({"this_hook": "should never run either"}); }); - var response = await client.get("$url/todos"); - print(response.body); - List result = god.deserialize(response.body); - expect(result[0]["angel"], equals("framework")); - }); -} \ No newline at end of file + var response = await client.get("$url/todos"); + print(response.body); + List result = god.deserialize(response.body); + expect(result[0]["angel"], equals("framework")); + }); +} diff --git a/test/routing.dart b/test/routing.dart index 6acb9a8f..d4249567 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -26,7 +26,7 @@ main() { http.Client client; setUp(() async { - final debug = false; + final debug = true; angel = new Angel(debug: debug); nested = new Angel(debug: debug); todos = new Angel(debug: debug); @@ -38,7 +38,7 @@ main() { }) ..registerMiddleware('intercept_service', (RequestContext req, res) async { - print("Intercepting a service!"); + res.write("Service with "); return true; }); @@ -48,7 +48,8 @@ main() { ted = nested.post('/ted/:route', (RequestContext req, res) { print('Params: ${req.params}'); - print('Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}'); + print( + 'Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}'); return req.params; }); @@ -75,7 +76,9 @@ main() { angel.use('/query', new QueryService()); angel.get('*', 'MJ'); - angel.dumpTree(header: "DUMPING ROUTES:"); + angel + ..normalize() + ..dumpTree(header: "DUMPING ROUTES:", showMatchers: true); client = new http.Client(); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); @@ -101,6 +104,7 @@ main() { var response = await client.get('$url/name/HELLO/last/WORLD'); print(response.body); var json = god.deserialize(response.body); + expect(json, new isInstanceOf>()); expect(json['first'], equals('HELLO')); expect(json['last'], equals('WORLD')); }); @@ -165,6 +169,6 @@ main() { response = await client.get("$url/query/foo?bar=baz"); print(response.body); - expect(response.body, equals("Middleware")); + expect(response.body, equals("Service with Middleware")); }); }