Hooks redesigned

This commit is contained in:
regiostech 2016-06-21 00:19:43 -04:00
parent 6cc9967b3e
commit 42023ca374
9 changed files with 300 additions and 111 deletions

View file

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

View file

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

View file

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

View file

@ -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<HookedServiceEvent> _beforeIndexed = new StreamController<HookedServiceEvent>.broadcast();
StreamController<HookedServiceEvent> _beforeRead = new StreamController.broadcast();
StreamController<HookedServiceEvent> _beforeCreated = new StreamController.broadcast();
StreamController<HookedServiceEvent> _beforeModified = new StreamController.broadcast();
StreamController<HookedServiceEvent> _beforeUpdated = new StreamController.broadcast();
StreamController<HookedServiceEvent> _beforeRemoved = new StreamController.broadcast();
Stream<HookedServiceEvent> get beforeIndexed => _beforeIndexed.stream;
Stream<HookedServiceEvent> get beforeRead => _beforeRead.stream;
Stream<HookedServiceEvent> get beforeCreated => _beforeCreated.stream;
Stream<HookedServiceEvent> get beforeModified => _beforeModified.stream;
Stream<HookedServiceEvent> get beforeUpdated => _beforeUpdated.stream;
Stream<HookedServiceEvent> get beforeRemoved => _beforeRemoved.stream;
StreamController<HookedServiceEvent> _afterIndexed = new StreamController<HookedServiceEvent>.broadcast();
StreamController<HookedServiceEvent> _afterRead = new StreamController<HookedServiceEvent>.broadcast();
StreamController<HookedServiceEvent> _afterCreated = new StreamController<HookedServiceEvent>.broadcast();
StreamController<HookedServiceEvent> _afterModified = new StreamController<HookedServiceEvent>.broadcast();
StreamController<HookedServiceEvent> _afterUpdated = new StreamController<HookedServiceEvent>.broadcast();
StreamController<HookedServiceEvent> _afterRemoved = new StreamController<HookedServiceEvent>.broadcast();
Stream<HookedServiceEvent> get afterIndexed => _afterIndexed.stream;
Stream<HookedServiceEvent> get afterRead => _afterRead.stream;
Stream<HookedServiceEvent> get afterCreated => _afterCreated.stream;
Stream<HookedServiceEvent> get afterModified => _afterModified.stream;
Stream<HookedServiceEvent> get afterUpdated => _afterUpdated.stream;
Stream<HookedServiceEvent> 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<List> 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;
}
}
}
/// Triggered on a hooked service event.
typedef Future HookedServiceEventListener(HookedServiceEvent event);
/// Can be listened to, but events may be canceled.
class HookedServiceEventDispatcher {
List<HookedServiceEventListener> listeners = [];
/// Fires an event, and returns it once it is either canceled, or all listeners have run.
Future<HookedServiceEvent> _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);
}
}

View file

@ -30,14 +30,15 @@ class MemoryService<T> 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 {

View file

@ -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 <thosakwe@gmail.com>
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"
http: ^0.11.3
test: ^0.12.13

89
test/hooked.dart Normal file
View file

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

View file

@ -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<Todo>();
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;

View file

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