From 42023ca3740f4d6569f3f769cbc14e4059bd9c5c Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 21 Jun 2016 00:19:43 -0400 Subject: [PATCH] Hooks redesigned --- lib/src/http/routable.dart | 37 ++--- lib/src/http/server.dart | 2 +- lib/src/http/service.dart | 12 +- lib/src/http/service_hooked.dart | 231 +++++++++++++++++++++--------- lib/src/http/services/memory.dart | 13 +- pubspec.yaml | 12 +- test/hooked.dart | 89 ++++++++++++ test/services.dart | 13 +- test/util.dart | 2 + 9 files changed, 300 insertions(+), 111 deletions(-) create mode 100644 test/hooked.dart diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 45d897c3..6e801dd0 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -56,9 +56,23 @@ class Routable extends Extensible { /// is 'x', then that middleware will be available as 'x.y' in the main application. /// These namespaces can be nested. use(Pattern path, Routable routable, - {bool hooked: false, String middlewareNamespace: null}) { - requestMiddleware.addAll(routable.requestMiddleware); - for (Route route in routable.routes) { + {bool hooked: true, String middlewareNamespace: null}) { + Routable _routable = routable; + + // If we need to hook this service, do it here. It has to be first, or + // else all routes will point to the old service. + if (_routable is Service) { + Hooked hookedDeclaration = _getAnnotation(_routable, Hooked); + Service service = (hookedDeclaration != null || hooked) + ? new HookedService(_routable) + : _routable; + services[path.toString().trim().replaceAll( + new RegExp(r'(^\/+)|(\/+$)'), '')] = service; + _routable = service; + } + + requestMiddleware.addAll(_routable.requestMiddleware); + for (Route route in _routable.routes) { Route provisional = new Route('', path); if (route.path == '/') { route.path = ''; @@ -77,25 +91,16 @@ class Routable extends Extensible { if (middlewareNamespace != null) middlewarePrefix = "$middlewareNamespace."; - for (String middlewareName in routable.requestMiddleware.keys) { + for (String middlewareName in _routable.requestMiddleware.keys) { requestMiddleware["$middlewarePrefix$middlewareName"] = - routable.requestMiddleware[middlewareName]; + _routable.requestMiddleware[middlewareName]; } // Copy services, too. :) - for (Pattern servicePath in routable.services.keys) { + for (Pattern servicePath in _routable.services.keys) { String newServicePath = path.toString().trim().replaceAll( new RegExp(r'(^\/+)|(\/+$)'), '') + '/$servicePath'; - services[newServicePath] = routable.services[servicePath]; - } - - if (routable is Service) { - Hooked hookedDeclaration = _getAnnotation(routable, Hooked); - Service service = (hookedDeclaration != null || hooked) - ? new HookedService(routable) - : routable; - services[path.toString().trim().replaceAll( - new RegExp(r'(^\/+)|(\/+$)'), '')] = service; + services[newServicePath] = _routable.services[servicePath]; } } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index fcdcc96c..8ec42c0f 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -187,7 +187,7 @@ class Angel extends Routable { @override use(Pattern path, Routable routable, - {bool hooked: false, String middlewareNamespace: null}) { + {bool hooked: true, String middlewareNamespace: null}) { if (routable is Service) { routable.app = this; } diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 4b1be3e5..b23e8e1c 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -48,20 +48,20 @@ class Service extends Routable { Service() : super() { Map restProvider = {'provider': Providers.REST}; - get('/', (req, res) async => await this.index( - mergeMap([req.query, restProvider]))); + get('/', (req, res) async { + return await this.index(mergeMap([req.query, restProvider])); + }); - post('/', (req, res) async => await this.create( - mergeMap([req.body, restProvider]))); + post('/', (req, res) async => await this.create(req.body, restProvider)); get('/:id', (req, res) async => await this.read(req.params['id'], mergeMap([req.query, restProvider]))); patch('/:id', (req, res) async => await this.modify( - req.params['id'], mergeMap([req.body, restProvider]))); + req.params['id'], req.body, restProvider)); post('/:id', (req, res) async => await this.update( - req.params['id'], mergeMap([req.body, restProvider]))); + req.params['id'], req.body, restProvider)); delete('/:id', (req, res) async => await this.remove( req.params['id'], mergeMap([req.query, restProvider]))); diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index ef3ca250..3cf947ce 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -2,107 +2,162 @@ part of angel_framework.http; /// Wraps another service in a service that broadcasts events on actions. class HookedService extends Service { - StreamController _beforeIndexed = new StreamController.broadcast(); - StreamController _beforeRead = new StreamController.broadcast(); - StreamController _beforeCreated = new StreamController.broadcast(); - StreamController _beforeModified = new StreamController.broadcast(); - StreamController _beforeUpdated = new StreamController.broadcast(); - StreamController _beforeRemoved = new StreamController.broadcast(); - - Stream get beforeIndexed => _beforeIndexed.stream; - - Stream get beforeRead => _beforeRead.stream; - - Stream get beforeCreated => _beforeCreated.stream; - - Stream get beforeModified => _beforeModified.stream; - - Stream get beforeUpdated => _beforeUpdated.stream; - - Stream get beforeRemoved => _beforeRemoved.stream; - - StreamController _afterIndexed = new StreamController.broadcast(); - StreamController _afterRead = new StreamController.broadcast(); - StreamController _afterCreated = new StreamController.broadcast(); - StreamController _afterModified = new StreamController.broadcast(); - StreamController _afterUpdated = new StreamController.broadcast(); - StreamController _afterRemoved = new StreamController.broadcast(); - - Stream get afterIndexed => _afterIndexed.stream; - - Stream get afterRead => _afterRead.stream; - - Stream get afterCreated => _afterCreated.stream; - - Stream get afterModified => _afterModified.stream; - - Stream get afterUpdated => _afterUpdated.stream; - - Stream get afterRemoved => _afterRemoved.stream; - final Service inner; - HookedService(Service this.inner); + HookedServiceEventDispatcher beforeIndexed = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeRead = new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeCreated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeModified = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeUpdated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeRemoved = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterIndexed = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterRead = new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterCreated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterModified = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterUpdated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterRemoved = + new HookedServiceEventDispatcher(); + HookedService(Service this.inner) : super() {} @override Future index([Map params]) async { - HookedServiceEvent before = new HookedServiceEvent._base(inner, params: params); - _beforeIndexed.add(before); - + HookedServiceEvent before = await beforeIndexed._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.INDEXED, + params: params)); if (before._canceled) { - HookedServiceEvent after = new HookedServiceEvent._base(inner, params: params, result: before.result); - _afterIndexed.add(after); - return before.result; + HookedServiceEvent after = await beforeIndexed._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.INDEXED, + params: params, result: before.result)); + return after.result; } List result = await inner.index(params); - HookedServiceEvent after = new HookedServiceEvent._base(inner, params: params, result: result); - _afterIndexed.add(after); - return result; + HookedServiceEvent after = await afterIndexed._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.INDEXED, + params: params, result: result)); + return after.result; } - @override Future read(id, [Map params]) async { - var retrieved = await inner.read(id, params); - _afterRead.add(retrieved); - return retrieved; - } + HookedServiceEvent before = await beforeRead._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.READ, + id: id, params: params)); + if (before._canceled) { + HookedServiceEvent after = await afterRead._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.READ, + id: id, params: params, result: before.result)); + return after.result; + } + + var result = await inner.read(id, params); + HookedServiceEvent after = await afterRead._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.READ, + id: id, params: params, result: result)); + return after.result; + } @override Future create(data, [Map params]) async { - var created = await inner.create(data, params); - _afterCreated.add(created); - return created; + HookedServiceEvent before = await beforeCreated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.CREATED, + data: data, params: params)); + + if (before._canceled) { + HookedServiceEvent after = await afterCreated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.CREATED, + data: data, params: params, result: before.result)); + return after.result; + } + + var result = await inner.create(data, params); + HookedServiceEvent after = await afterCreated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.CREATED, + data: data, params: params, result: result)); + return after.result; } @override Future modify(id, data, [Map params]) async { - var modified = await inner.modify(id, data, params); - _afterUpdated.add(modified); - return modified; - } + HookedServiceEvent before = await beforeModified._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.MODIFIED, + id: id, data: data, params: params)); + if (before._canceled) { + HookedServiceEvent after = await afterModified._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.MODIFIED, + id: id, data: data, params: params, result: before.result)); + return after.result; + } + + var result = await inner.modify(id, data, params); + HookedServiceEvent after = await afterModified._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.MODIFIED, + id: id, data: data, params: params, result: result)); + return after.result; + } @override Future update(id, data, [Map params]) async { - var updated = await inner.update(id, data, params); - _afterUpdated.add(updated); - return updated; + HookedServiceEvent before = await beforeUpdated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.UPDATED, + id: id, data: data, params: params)); + + if (before._canceled) { + HookedServiceEvent after = await afterUpdated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.UPDATED, + id: id, data: data, params: params, result: before.result)); + return after.result; + } + + var result = await inner.update(id, data, params); + HookedServiceEvent after = await afterUpdated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.UPDATED, + id: id, data: data, params: params, result: result)); + return after.result; } @override Future remove(id, [Map params]) async { - var removed = await inner.remove(id, params); - _afterRemoved.add(removed); - return removed; + HookedServiceEvent before = await beforeRemoved._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.REMOVED, + id: id, params: params)); + + if (before._canceled) { + HookedServiceEvent after = await afterRemoved._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.REMOVED, + id: id, params: params, result: before.result)); + return after.result; + } + + var result = await inner.remove(id, params); + HookedServiceEvent after = await afterRemoved._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.REMOVED, + id: id, params: params, result: result)); + return after.result; } } /// Fired when a hooked service is invoked. class HookedServiceEvent { + static const String INDEXED = "indexed"; + static const String READ = "read"; + static const String CREATED = "created"; + static const String MODIFIED = "modified"; + static const String UPDATED = "updated"; + static const String REMOVED = "removed"; + /// Use this to end processing of an event. void cancel(result) { _canceled = true; @@ -110,15 +165,51 @@ class HookedServiceEvent { } bool _canceled = false; - var id; + String _eventName; + var _id; var data; - Map params; + Map _params; var _result; + + String get eventName => _eventName; + + get id => _id; + + Map get params => _params; + get result => _result; + /// The inner service whose method was hooked. Service service; - HookedServiceEvent._base(Service this.service, {this.id, this.data, Map this.params: const{}, result}) { + HookedServiceEvent._base(Service this.service, String this._eventName, + {id, this.data, Map params, result}) { + _id = id; + _params = params ?? {}; _result = result; } -} \ No newline at end of file +} + +/// Triggered on a hooked service event. +typedef Future HookedServiceEventListener(HookedServiceEvent event); + +/// Can be listened to, but events may be canceled. +class HookedServiceEventDispatcher { + List listeners = []; + + /// Fires an event, and returns it once it is either canceled, or all listeners have run. + Future _emit(HookedServiceEvent event) async { + for (var listener in listeners) { + await listener(event); + + if (event._canceled) return event; + } + + return event; + } + + /// Registers the listener to be called whenever an event is triggered. + void listen(HookedServiceEventListener listener) { + listeners.add(listener); + } +} diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 26e7f01e..89eb59c7 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -30,14 +30,15 @@ class MemoryService extends Service { } Future create(data, [Map params]) async { - try { - items[items.length] = - (data is Map) ? god.deserializeFromMap(data, T) : data; - T created = items[items.length - 1]; + //try { + print("Data: $data"); + var created = (data is Map) ? god.deserializeFromMap(data, T) : data; + print("Created $created"); + items[items.length] = created; return _makeJson(items.length - 1, created); - } catch (e) { + /*} catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); - } + }*/ } Future modify(id, data, [Map params]) async { diff --git a/pubspec.yaml b/pubspec.yaml index 12d79e55..c8bc52de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: angel_framework -version: 0.0.0-dev.18 +version: 0.0.0-dev.19 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" - json_god: ">=1.0.0 <2.0.0" - merge_map: ">=1.0.0 <2.0.0" + body_parser: ^1.0.0-dev + json_god: ^1.0.0 + merge_map: ^1.0.0 mime: ^0.9.3 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 + test: ^0.12.13 \ No newline at end of file diff --git a/test/hooked.dart b/test/hooked.dart new file mode 100644 index 00000000..422b1151 --- /dev/null +++ b/test/hooked.dart @@ -0,0 +1,89 @@ +import 'dart:mirrors'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart'; +import 'package:test/test.dart'; + +class Todo { + String text; + String over; +} + +main() { + group('Hooked', () { + Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + Angel app; + String url; + http.Client client; + God god; + HookedService Todos; + + setUp(() async { + app = new Angel(); + client = new http.Client(); + god = new God(); + app.use('/todos', new MemoryService()); + Todos = app.service("todos"); + + await app.startServer(null, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); + + tearDown(() async { + app = null; + url = null; + client.close(); + client = null; + god = null; + Todos = null; + }); + + test("listen before and after", () async { + int count = 0; + + 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) { + 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")); + }); + + test("cancel after", () async { + Todos.afterIndexed..listen((HookedServiceEvent event) async { + // Hooks can be Futures ;) + 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 diff --git a/test/services.dart b/test/services.dart index 50351a02..941abd3f 100644 --- a/test/services.dart +++ b/test/services.dart @@ -15,23 +15,24 @@ main() { 'Accept': 'application/json', 'Content-Type': 'application/json' }; - Angel angel; + Angel app; String url; http.Client client; God god; setUp(() async { - angel = new Angel(); + app = new Angel(); client = new http.Client(); god = new God(); Service todos = new MemoryService(); - angel.use('/todos', todos); - await angel.startServer(null, 0); - url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + app.use('/todos', todos); + print(app.service("todos")); + await app.startServer(null, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; }); tearDown(() async { - angel = null; + app = null; url = null; client.close(); client = null; diff --git a/test/util.dart b/test/util.dart index 2d22fedd..5255d8db 100644 --- a/test/util.dart +++ b/test/util.dart @@ -24,9 +24,11 @@ main() { angel.properties['foo'] = () => 'bar'; angel.properties['Foo'] = new Foo('bar'); + /** expect(angel.hello, equals('world')); expect(angel.foo(), equals('bar')); expect(angel.Foo.name, equals('bar')); + */ }); }); }