Hooks redesigned
This commit is contained in:
parent
6cc9967b3e
commit
42023ca374
9 changed files with 300 additions and 111 deletions
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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])));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
12
pubspec.yaml
12
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 <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
89
test/hooked.dart
Normal 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"));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
*/
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue