This commit is contained in:
Tobe O 2018-06-08 03:06:26 -04:00
parent ef5a28b956
commit 954e8141dd
38 changed files with 1315 additions and 1388 deletions

View file

@ -519,6 +519,7 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/yaml-2.1.13/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/yaml-2.1.13/lib" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<LIBRARY_FILE />
<SOURCES /> <SOURCES />
</library> </library>
</component> </component>

View file

@ -21,6 +21,7 @@
<root url="file:///usr/local/opt/dart/libexec/lib/web_sql" /> <root url="file:///usr/local/opt/dart/libexec/lib/web_sql" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<LIBRARY_FILE />
<SOURCES /> <SOURCES />
</library> </library>
</component> </component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Match routes, even with query params in routing_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="Match routes, even with query params" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="can fetch data in services_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/services_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="can fetch data" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,8 @@
# 1.1.4
* Remove all usages of `async`/`await` in the core library.
* `AngelConfigurer` now accepts `FutureOr`.
* `AngelHttp` now has a `useZone` flag to disable custom zones.
# 1.1.3 # 1.1.3
* `AngelHttp` now handles requests in a `Zone`. * `AngelHttp` now handles requests in a `Zone`.
* Use `package:dart2_constant`. * Use `package:dart2_constant`.

View file

@ -1,2 +1,5 @@
analyzer: analyzer:
strong-mode: true strong-mode: true
linter:
rules:
- avoid_slow_async_io

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:dart2_constant/convert.dart'; import 'package:dart2_constant/convert.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
@ -48,6 +47,6 @@ serverMain(_) async {
print(e.stackTrace); print(e.stackTrace);
}; };
var server = await http.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); var server = await http.startServer('127.0.0.1', 3000);
print('Listening at http://${server.address.address}:${server.port}'); print('Listening at http://${server.address.address}:${server.port}');
} }

View file

@ -9,19 +9,19 @@ import 'angel_framework.dart';
/// Sequentially runs a set of [listeners]. /// Sequentially runs a set of [listeners].
HookedServiceEventListener chainListeners( HookedServiceEventListener chainListeners(
Iterable<HookedServiceEventListener> listeners) { Iterable<HookedServiceEventListener> listeners) {
return (HookedServiceEvent e) async { return (HookedServiceEvent e) {
for (HookedServiceEventListener listener in listeners) await listener(e); for (HookedServiceEventListener listener in listeners) listener(e);
}; };
} }
/// Runs a [callback] on every service, and listens for future services to run it again. /// Runs a [callback] on every service, and listens for future services to run it again.
AngelConfigurer hookAllServices(callback(Service service)) { AngelConfigurer hookAllServices(callback(Service service)) {
return (Angel app) async { return (Angel app) {
List<Service> touched = []; List<Service> touched = [];
for (var service in app.services.values) { for (var service in app.services.values) {
if (!touched.contains(service)) { if (!touched.contains(service)) {
await callback(service); callback(service);
touched.add(service); touched.add(service);
} }
} }
@ -52,9 +52,9 @@ HookedServiceEventListener transform(transformer(obj), [condition]) {
Iterable cond = condition is Iterable ? condition : [condition]; Iterable cond = condition is Iterable ? condition : [condition];
if (condition == null) cond = []; if (condition == null) cond = [];
_condition(HookedServiceEvent e, condition) async { _condition(HookedServiceEvent e, condition) {
if (condition is Function) if (condition is Function)
return await condition(e); return condition(e);
else if (condition == Providers) else if (condition == Providers)
return true; return true;
else { else {
@ -70,11 +70,11 @@ HookedServiceEventListener transform(transformer(obj), [condition]) {
} }
} }
normalize(HookedServiceEvent e, obj) async { normalize(HookedServiceEvent e, obj) {
bool transform = true; bool transform = true;
for (var c in cond) { for (var c in cond) {
var r = await _condition(e, c); var r = _condition(e, c);
if (r != true) { if (r != true) {
transform = false; transform = false;
@ -97,7 +97,7 @@ HookedServiceEventListener transform(transformer(obj), [condition]) {
var r = []; var r = [];
for (var o in obj) { for (var o in obj) {
r.add(await normalize(e, o)); r.add(normalize(e, o));
} }
return r; return r;
@ -105,10 +105,10 @@ HookedServiceEventListener transform(transformer(obj), [condition]) {
return transformer(obj); return transformer(obj);
} }
return (HookedServiceEvent e) async { return (HookedServiceEvent e) {
if (e.isBefore) { if (e.isBefore) {
e.data = await normalize(e, e.data); e.data = normalize(e, e.data);
} else if (e.isAfter) e.result = await normalize(e, e.result); } else if (e.isAfter) e.result = normalize(e, e.result);
}; };
} }
@ -135,7 +135,7 @@ HookedServiceEventListener toType(Type type) {
/// Only applies to the client-side. /// Only applies to the client-side.
HookedServiceEventListener remove(key, [remover(key, obj)]) { HookedServiceEventListener remove(key, [remover(key, obj)]) {
return (HookedServiceEvent e) async { return (HookedServiceEvent e) async {
_remover(key, obj) { _remover(key, obj) async {
if (remover != null) if (remover != null)
return remover(key, obj); return remover(key, obj);
else if (obj is List) else if (obj is List)
@ -171,15 +171,15 @@ HookedServiceEventListener remove(key, [remover(key, obj)]) {
if (obj is Iterable) { if (obj is Iterable) {
return await Future.wait(obj.map(_removeAll)); return await Future.wait(obj.map(_removeAll));
} else } else
return await _removeAll(obj); return _removeAll(obj);
} }
} }
if (e.params?.containsKey('provider') == true) { if (e.params?.containsKey('provider') == true) {
if (e.isBefore) { if (e.isBefore) {
e.data = await normalize(e.data); e.data = normalize(e.data);
} else if (e.isAfter) { } else if (e.isAfter) {
e.result = await normalize(e.result); e.result = normalize(e.result);
} }
} }
}; };
@ -193,12 +193,12 @@ HookedServiceEventListener remove(key, [remover(key, obj)]) {
/// ///
/// If [provider] is `null`, then it will be disabled to all clients. /// If [provider] is `null`, then it will be disabled to all clients.
HookedServiceEventListener disable([provider]) { HookedServiceEventListener disable([provider]) {
return (HookedServiceEvent e) async { return (HookedServiceEvent e) {
if (e.params.containsKey('provider')) { if (e.params.containsKey('provider')) {
if (provider == null) if (provider == null)
throw new AngelHttpException.methodNotAllowed(); throw new AngelHttpException.methodNotAllowed();
else if (provider is Function) { else if (provider is Function) {
var r = await provider(e); var r = provider(e);
if (r != true) throw new AngelHttpException.methodNotAllowed(); if (r != true) throw new AngelHttpException.methodNotAllowed();
} else { } else {
_provide(p) => p is Providers ? p : new Providers(p.toString()); _provide(p) => p is Providers ? p : new Providers(p.toString());
@ -224,7 +224,7 @@ HookedServiceEventListener addCreatedAt(
{assign(obj, now), String key, bool serialize: true}) { {assign(obj, now), String key, bool serialize: true}) {
var name = key?.isNotEmpty == true ? key : 'createdAt'; var name = key?.isNotEmpty == true ? key : 'createdAt';
return (HookedServiceEvent e) async { return (HookedServiceEvent e) {
_assign(obj, now) { _assign(obj, now) {
if (assign != null) if (assign != null)
return assign(obj, now); return assign(obj, now);
@ -242,17 +242,17 @@ HookedServiceEventListener addCreatedAt(
var d = new DateTime.now().toUtc(); var d = new DateTime.now().toUtc();
var now = serialize == false ? d : d.toIso8601String(); var now = serialize == false ? d : d.toIso8601String();
normalize(obj) async { normalize(obj) {
if (obj != null) { if (obj != null) {
if (obj is Iterable) { if (obj is Iterable) {
obj.forEach(normalize); obj.forEach(normalize);
} else { } else {
await _assign(obj, now); _assign(obj, now);
} }
} }
} }
await normalize(e.isBefore ? e.data : e.result); normalize(e.isBefore ? e.data : e.result);
}; };
} }
@ -265,7 +265,7 @@ HookedServiceEventListener addUpdatedAt(
{assign(obj, now), String key, bool serialize: true}) { {assign(obj, now), String key, bool serialize: true}) {
var name = key?.isNotEmpty == true ? key : 'updatedAt'; var name = key?.isNotEmpty == true ? key : 'updatedAt';
return (HookedServiceEvent e) async { return (HookedServiceEvent e) {
_assign(obj, now) { _assign(obj, now) {
if (assign != null) if (assign != null)
return assign(obj, now); return assign(obj, now);
@ -283,16 +283,16 @@ HookedServiceEventListener addUpdatedAt(
var d = new DateTime.now().toUtc(); var d = new DateTime.now().toUtc();
var now = serialize == false ? d : d.toIso8601String(); var now = serialize == false ? d : d.toIso8601String();
normalize(obj) async { normalize(obj) {
if (obj != null) { if (obj != null) {
if (obj is Iterable) { if (obj is Iterable) {
obj.forEach(normalize); obj.forEach(normalize);
} else { } else {
await _assign(obj, now); _assign(obj, now);
} }
} }
} }
await normalize(e.isBefore ? e.data : e.result); normalize(e.isBefore ? e.data : e.result);
}; };
} }

View file

@ -9,9 +9,8 @@ typedef Future<String> ViewGenerator(String path, [Map data]);
/// Base class for Angel servers. Do not bother extending this. /// Base class for Angel servers. Do not bother extending this.
class AngelBase extends Routable { class AngelBase extends Routable {
static ViewGenerator noViewEngineConfigured = (String view, static ViewGenerator noViewEngineConfigured = (String view, [Map data]) =>
[Map data]) async => new Future<String>.value("No view engine has been configured yet.");
"No view engine has been configured yet.";
Container _container = new Container(); Container _container = new Container();
@ -35,9 +34,10 @@ class AngelBase extends Routable {
ViewGenerator viewGenerator = noViewEngineConfigured; ViewGenerator viewGenerator = noViewEngineConfigured;
/// Closes this instance, rendering it **COMPLETELY DEFUNCT**. /// Closes this instance, rendering it **COMPLETELY DEFUNCT**.
Future close() async { Future close() {
await super.close(); super.close();
_container = null; _container = null;
viewGenerator = noViewEngineConfigured; viewGenerator = noViewEngineConfigured;
return new Future.value();
} }
} }

View file

@ -67,7 +67,7 @@ class HookedService extends Service {
/// Closes any open [StreamController]s on this instance. **Internal use only**. /// Closes any open [StreamController]s on this instance. **Internal use only**.
@override @override
Future close() async { Future close() {
_ctrl.forEach((c) => c.close()); _ctrl.forEach((c) => c.close());
beforeIndexed._close(); beforeIndexed._close();
beforeRead._close(); beforeRead._close();
@ -81,7 +81,8 @@ class HookedService extends Service {
afterModified._close(); afterModified._close();
afterUpdated._close(); afterUpdated._close();
afterRemoved._close(); afterRemoved._close();
await inner.close(); inner.close();
return new Future.value();
} }
/// Adds hooks to this instance. /// Adds hooks to this instance.
@ -120,149 +121,16 @@ class HookedService extends Service {
applyListeners(inner.remove, afterRemoved, true); applyListeners(inner.remove, afterRemoved, true);
} }
/// Adds routes to this instance. List get bootstrappers => new List.from(super.bootstrappers)
@override ..add((RequestContext req, ResponseContext res) {
void addRoutes() { req.serviceParams
// Set up our routes. We still need to copy middleware from inner service ..['__requestctx'] = req
Map restProvider = {'provider': Providers.rest}; ..['__responsectx'] = res;
return true;
});
// Add global middleware if declared on the instance itself void addRoutes([Service s]) {
Middleware before = getAnnotation(inner, Middleware); super.addRoutes(s ?? inner);
List handlers = [
(RequestContext req, ResponseContext res) async {
req.serviceParams
..['__requestctx'] = req
..['__responsectx'] = res;
return true;
}
];
if (before != null) handlers.addAll(before.handlers);
Middleware indexMiddleware = getAnnotation(inner.index, Middleware);
get('/', (req, res) async {
return await this.index(mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(inner.create, Middleware);
post('/', (req, res) async {
var r = await this.create(
await req.lazyBody(),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
]));
res.statusCode = 201;
return r;
},
middleware: []
..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(inner.read, Middleware);
get(
'/:id',
(req, res) async => await this.read(
toId(req.params['id']),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(inner.modify, Middleware);
patch(
'/:id',
(req, res) async => await this.modify(
toId(req.params['id']),
await req.lazyBody(),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []
..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(inner.update, Middleware);
post(
'/:id',
(req, res) async => await this.update(
toId(req.params['id']),
await req.lazyBody(),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
put(
'/:id',
(req, res) async => await this.update(
toId(req.params['id']),
await req.lazyBody(),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(inner.remove, Middleware);
delete(
'/',
(req, res) async => await this.remove(
null,
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
delete(
'/:id',
(req, res) async => await this.remove(
toId(req.params['id']),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
// REST compliance
put('/', () => throw new AngelHttpException.notFound());
patch('/', () => throw new AngelHttpException.notFound());
addHooks();
} }
/// Runs the [listener] before every service method specified. /// Runs the [listener] before every service method specified.
@ -388,192 +256,165 @@ class HookedService extends Service {
} }
@override @override
Future index([Map _params]) async { Future index([Map _params]) {
var params = _stripReq(_params); var params = _stripReq(_params);
HookedServiceEvent before = await beforeIndexed._emit( return beforeIndexed
new HookedServiceEvent(false, _getRequest(_params), ._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed, _getResponse(_params), inner, HookedServiceEvent.indexed,
params: params)); params: params))
if (before._canceled) { .then((before) {
HookedServiceEvent after = await beforeIndexed._emit( if (before._canceled) {
new HookedServiceEvent(true, _getRequest(_params), return beforeIndexed
_getResponse(_params), inner, HookedServiceEvent.indexed, ._emit(new HookedServiceEvent(true, _getRequest(_params),
params: params, result: before.result)); _getResponse(_params), inner, HookedServiceEvent.indexed,
return after.result; params: params, result: before.result))
} .then((after) => after.result);
}
var result = await inner.index(params); return inner.index(params).then((result) {
HookedServiceEvent after = await afterIndexed._emit(new HookedServiceEvent( return afterIndexed
true, ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getRequest(_params), _getResponse(_params), inner, HookedServiceEvent.indexed,
_getResponse(_params), params: params, result: result))
inner, .then((after) => after.result);
HookedServiceEvent.indexed, });
params: params, });
result: result));
return after.result;
} }
@override @override
Future read(id, [Map _params]) async { Future read(id, [Map _params]) {
var params = _stripReq(_params); var params = _stripReq(_params);
HookedServiceEvent before = await beforeRead._emit(new HookedServiceEvent( return beforeRead
false, ._emit(new HookedServiceEvent(false, _getRequest(_params),
_getRequest(_params), _getResponse(_params), inner, HookedServiceEvent.read,
_getResponse(_params), id: id, params: params))
inner, .then((before) {
HookedServiceEvent.indexed, if (before._canceled) {
id: id, return beforeRead
params: params)); ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params, result: before.result))
.then((after) => after.result);
}
if (before._canceled) { return inner.read(id, params).then((result) {
HookedServiceEvent after = await afterRead._emit(new HookedServiceEvent( return afterRead
true, ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getRequest(_params), _getResponse(_params), inner, HookedServiceEvent.read,
_getResponse(_params), id: id, params: params, result: result))
inner, .then((after) => after.result);
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(
true,
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.read,
id: id,
params: params,
result: result));
return after.result;
} }
@override @override
Future create(data, [Map _params]) async { Future create(data, [Map _params]) {
var params = _stripReq(_params); var params = _stripReq(_params);
HookedServiceEvent before = await beforeCreated._emit( return beforeCreated
new HookedServiceEvent(false, _getRequest(_params), ._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created, _getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params)); data: data, params: params))
.then((before) {
if (before._canceled) {
return beforeCreated
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params, result: before.result))
.then((after) => after.result);
}
if (before._canceled) { return inner.create(data, params).then((result) {
HookedServiceEvent after = await afterCreated._emit( return afterCreated
new HookedServiceEvent(true, _getRequest(_params), ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created, _getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params, result: before.result)); data: data, params: params, result: result))
return after.result; .then((after) => after.result);
} });
});
var result = await inner.create(data, params);
HookedServiceEvent after = await afterCreated._emit(new HookedServiceEvent(
true,
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.created,
data: data,
params: params,
result: result));
return after.result;
} }
@override @override
Future modify(id, data, [Map _params]) async { Future modify(id, data, [Map _params]) {
var params = _stripReq(_params); var params = _stripReq(_params);
HookedServiceEvent before = await beforeModified._emit( return beforeModified
new HookedServiceEvent(false, _getRequest(_params), ._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified, _getResponse(_params), inner, HookedServiceEvent.modified,
id: id, data: data, params: params)); id: id, data: data, params: params))
.then((before) {
if (before._canceled) {
return beforeModified
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified,
id: id, data: data, params: params, result: before.result))
.then((after) => after.result);
}
if (before._canceled) { return inner.modify(id, data, params).then((result) {
HookedServiceEvent after = await afterModified._emit( return afterModified
new HookedServiceEvent(true, _getRequest(_params), ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified, _getResponse(_params), inner, HookedServiceEvent.created,
id: id, data: data, params: params, result: before.result)); id: id, data: data, params: params, result: result))
return after.result; .then((after) => after.result);
} });
});
var result = await inner.modify(id, data, params);
HookedServiceEvent after = await afterModified._emit(new HookedServiceEvent(
true,
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.modified,
id: id,
data: data,
params: params,
result: result));
return after.result;
} }
@override @override
Future update(id, data, [Map _params]) async { Future update(id, data, [Map _params]) {
var params = _stripReq(_params); var params = _stripReq(_params);
HookedServiceEvent before = await beforeUpdated._emit( return beforeUpdated
new HookedServiceEvent(false, _getRequest(_params), ._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated, _getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params)); id: id, data: data, params: params))
.then((before) {
if (before._canceled) {
return beforeUpdated
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params, result: before.result))
.then((after) => after.result);
}
if (before._canceled) { return inner.update(id, data, params).then((result) {
HookedServiceEvent after = await afterUpdated._emit( return afterUpdated
new HookedServiceEvent(true, _getRequest(_params), ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated, _getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params, result: before.result)); id: id, data: data, params: params, result: result))
return after.result; .then((after) => after.result);
} });
});
var result = await inner.update(id, data, params);
HookedServiceEvent after = await afterUpdated._emit(new HookedServiceEvent(
true,
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.updated,
id: id,
data: data,
params: params,
result: result));
return after.result;
} }
@override @override
Future remove(id, [Map _params]) async { Future remove(id, [Map _params]) {
var params = _stripReq(_params); var params = _stripReq(_params);
HookedServiceEvent before = await beforeRemoved._emit( return beforeRemoved
new HookedServiceEvent(false, _getRequest(_params), ._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed, _getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params)); id: id, params: params))
.then((before) {
if (before._canceled) {
return beforeRemoved
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, result: before.result))
.then((after) => after.result);
}
if (before._canceled) { return inner.remove(id, params).then((result) {
HookedServiceEvent after = await afterRemoved._emit( return afterRemoved
new HookedServiceEvent(true, _getRequest(_params), ._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed, _getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, result: before.result)); id: id, params: params, result: result))
return after.result; .then((after) => after.result);
} });
});
var result = await inner.remove(id, params);
HookedServiceEvent after = await afterRemoved._emit(new HookedServiceEvent(
true,
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.removed,
id: id,
params: params,
result: result));
return after.result;
} }
/// Fires an `after` event. This will not be propagated to clients, /// Fires an `after` event. This will not be propagated to clients,
/// but will be broadcasted to WebSockets, etc. /// but will be broadcasted to WebSockets, etc.
Future<HookedServiceEvent> fire(String eventName, result, Future<HookedServiceEvent> fire(String eventName, result,
[HookedServiceEventListener callback]) async { [HookedServiceEventListener callback]) {
HookedServiceEventDispatcher dispatcher; HookedServiceEventDispatcher dispatcher;
switch (eventName) { switch (eventName) {
@ -600,15 +441,15 @@ class HookedService extends Service {
} }
var ev = new HookedServiceEvent(true, null, null, this, eventName); var ev = new HookedServiceEvent(true, null, null, this, eventName);
return await fireEvent(dispatcher, ev, callback); return fireEvent(dispatcher, ev, callback);
} }
/// Sends an arbitrary event down the hook chain. /// Sends an arbitrary event down the hook chain.
Future<HookedServiceEvent> fireEvent( Future<HookedServiceEvent> fireEvent(
HookedServiceEventDispatcher dispatcher, HookedServiceEvent event, HookedServiceEventDispatcher dispatcher, HookedServiceEvent event,
[HookedServiceEventListener callback]) async { [HookedServiceEventListener callback]) {
if (callback != null && event?._canceled != true) await callback(event); if (callback != null && event?._canceled != true) callback(event);
return await dispatcher._emit(event); return dispatcher._emit(event);
} }
} }
@ -690,16 +531,16 @@ class HookedServiceEventDispatcher {
} }
/// Fires an event, and returns it once it is either canceled, or all listeners have run. /// Fires an event, and returns it once it is either canceled, or all listeners have run.
Future<HookedServiceEvent> _emit(HookedServiceEvent event) async { Future<HookedServiceEvent> _emit(HookedServiceEvent event) {
if (event?._canceled != true) { if (event?._canceled != true) {
for (var listener in listeners) { for (var listener in listeners) {
await listener(event); listener(event);
if (event._canceled) return event; if (event._canceled) return new Future.value(event);
} }
} }
return event; return new Future.value(event);
} }
/// Returns a [Stream] containing all events fired by this dispatcher. /// Returns a [Stream] containing all events fired by this dispatcher.

View file

@ -78,10 +78,11 @@ bool suitableForInjection(
/// Handles a request with a DI-enabled handler. /// Handles a request with a DI-enabled handler.
RequestHandler handleContained(handler, InjectionRequest injection) { RequestHandler handleContained(handler, InjectionRequest injection) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) {
if (injection.parameters.isNotEmpty && if (injection.parameters.isNotEmpty &&
injection.parameters.values.any((p) => p.match != null) && injection.parameters.values.any((p) => p.match != null) &&
!suitableForInjection(req, res, injection)) return true; !suitableForInjection(req, res, injection))
return new Future.value(true);
List args = []; List args = [];
@ -94,8 +95,7 @@ RequestHandler handleContained(handler, InjectionRequest injection) {
named[name] = resolveInjection([k, v], injection, req, res, false); named[name] = resolveInjection([k, v], injection, req, res, false);
}); });
var result = Function.apply(handler, args, named); return Function.apply(handler, args, named);
return result is Future ? await result : result;
}; };
} }
@ -181,6 +181,5 @@ InjectionRequest preInject(Function handler) {
injection.named[name] = type; injection.named[name] = type;
} }
} }
return injection; return injection;
} }

View file

@ -45,13 +45,13 @@ class MapService extends Service {
} }
@override @override
Future<List> index([Map params]) async { Future<List> index([Map params]) {
if (allowQuery == false || params == null || params['query'] is! Map) if (allowQuery == false || params == null || params['query'] is! Map)
return items; return new Future.value(items);
else { else {
Map query = params['query']; Map query = params['query'];
return items.where((item) { return new Future.value(items.where((item) {
for (var key in query.keys) { for (var key in query.keys) {
if (!item.containsKey(key)) if (!item.containsKey(key))
return false; return false;
@ -59,19 +59,19 @@ class MapService extends Service {
} }
return true; return true;
}).toList(); }).toList());
} }
} }
@override @override
Future<Map> read(id, [Map params]) async { Future<Map> read(id, [Map params]) {
return items.firstWhere(_matchesId(id), return new Future.value(items.firstWhere(_matchesId(id),
orElse: () => throw new AngelHttpException.notFound( orElse: () => throw new AngelHttpException.notFound(
message: 'No record found for ID $id')); message: 'No record found for ID $id')));
} }
@override @override
Future<Map> create(data, [Map params]) async { Future<Map> create(data, [Map params]) {
if (data is! Map) if (data is! Map)
throw new AngelHttpException.badRequest( throw new AngelHttpException.badRequest(
message: message:
@ -86,70 +86,71 @@ class MapService extends Service {
..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = now; ..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = now;
} }
items.add(result); items.add(result);
return result; return new Future.value(result);
} }
@override @override
Future<Map> modify(id, data, [Map params]) async { Future<Map> modify(id, data, [Map params]) {
if (data is! Map) if (data is! Map)
throw new AngelHttpException.badRequest( throw new AngelHttpException.badRequest(
message: message:
'MapService does not support `modify` with ${data.runtimeType}.'); 'MapService does not support `modify` with ${data.runtimeType}.');
if (!items.any(_matchesId(id))) return await create(data, params); if (!items.any(_matchesId(id))) return create(data, params);
var item = await read(id); return read(id).then((item) {
var result = item..addAll(data); var result = item..addAll(data);
if (autoIdAndDateFields == true) if (autoIdAndDateFields == true)
result result
..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = ..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] =
new DateTime.now().toIso8601String(); new DateTime.now().toIso8601String();
return result; return new Future.value(result);
});
} }
@override @override
Future<Map> update(id, data, [Map params]) async { Future<Map> update(id, data, [Map params]) {
if (data is! Map) if (data is! Map)
throw new AngelHttpException.badRequest( throw new AngelHttpException.badRequest(
message: message:
'MapService does not support `update` with ${data.runtimeType}.'); 'MapService does not support `update` with ${data.runtimeType}.');
if (!items.any(_matchesId(id))) return await create(data, params); if (!items.any(_matchesId(id))) return create(data, params);
var old = await read(id); return read(id).then((old) {
if (!items.remove(old))
throw new AngelHttpException.notFound(
message: 'No record found for ID $id');
if (!items.remove(old)) var result = data;
throw new AngelHttpException.notFound( if (autoIdAndDateFields == true) {
message: 'No record found for ID $id'); result
..['id'] = id?.toString()
var result = data; ..[autoSnakeCaseNames == false ? 'createdAt' : 'created_at'] =
if (autoIdAndDateFields == true) { old[autoSnakeCaseNames == false ? 'createdAt' : 'created_at']
result ..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] =
..['id'] = id?.toString() new DateTime.now().toIso8601String();
..[autoSnakeCaseNames == false ? 'createdAt' : 'created_at'] = }
old[autoSnakeCaseNames == false ? 'createdAt' : 'created_at'] items.add(result);
..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = return new Future.value(result);
new DateTime.now().toIso8601String(); });
}
items.add(result);
return result;
} }
@override @override
Future<Map> remove(id, [Map params]) async { Future<Map> remove(id, [Map params]) {
if (id == null || if (id == null ||
id == 'null' && id == 'null' &&
(allowRemoveAll == true || (allowRemoveAll == true ||
params?.containsKey('provider') != true)) { params?.containsKey('provider') != true)) {
items.clear(); items.clear();
return {}; return new Future.value({});
} }
var result = await read(id, params); return read(id, params).then((result) {
if (items.remove(result))
if (items.remove(result)) return result;
return result; else
else throw new AngelHttpException.notFound(
throw new AngelHttpException.notFound( message: 'No record found for ID $id');
message: 'No record found for ID $id'); });
} }
} }

View file

@ -10,6 +10,7 @@ import 'metadata.dart';
import 'response_context.dart'; import 'response_context.dart';
import 'routable.dart'; import 'routable.dart';
import 'server.dart' show Angel; import 'server.dart' show Angel;
part 'injection.dart'; part 'injection.dart';
/// A convenience wrapper around an incoming HTTP request. /// A convenience wrapper around an incoming HTTP request.
@ -167,8 +168,8 @@ abstract class RequestContext {
void inject(type, value) { void inject(type, value) {
if (!app.isProduction && type is Type) { if (!app.isProduction && type is Type) {
if (!reflect(value).type.isAssignableTo(reflectType(type))) if (!reflect(value).type.isAssignableTo(reflectType(type)))
throw new ArgumentError( throw new ArgumentError('Cannot inject $value (${value
'Cannot inject $value (${value.runtimeType}) as an instance of $type.'); .runtimeType}) as an instance of $type.');
} }
_injections[type] = value; _injections[type] = value;
@ -230,12 +231,12 @@ abstract class RequestContext {
} }
/// Manually parses the request body, if it has not already been parsed. /// Manually parses the request body, if it has not already been parsed.
Future<BodyParseResult> parse() async { Future<BodyParseResult> parse() {
if (_body != null) if (_body != null)
return _body; return new Future.value(_body);
else else
_provisionalQuery = null; _provisionalQuery = null;
return _body = await parseOnce(); return parseOnce().then((body) => _body = body);
} }
/// Override this method to one-time parse an incoming request. /// Override this method to one-time parse an incoming request.
@ -243,7 +244,7 @@ abstract class RequestContext {
Future<BodyParseResult> parseOnce(); Future<BodyParseResult> parseOnce();
/// Disposes of all resources. /// Disposes of all resources.
Future close() async { Future close() {
_body = null; _body = null;
_acceptsAllCache = null; _acceptsAllCache = null;
_acceptHeaderCache = null; _acceptHeaderCache = null;
@ -252,5 +253,6 @@ abstract class RequestContext {
_injections.clear(); _injections.clear();
serviceParams.clear(); serviceParams.clear();
params.clear(); params.clear();
return new Future.value();
} }
} }

View file

@ -123,10 +123,11 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
/// If `true`, all response finalizers will be skipped. /// If `true`, all response finalizers will be skipped.
bool willCloseItself = false; bool willCloseItself = false;
static StateError closed() => new StateError('Cannot modify a closed response.'); static StateError closed() =>
new StateError('Cannot modify a closed response.');
/// Sends a download as a response. /// Sends a download as a response.
Future download(File file, {String filename}) async { void download(File file, {String filename}) {
if (!isOpen) throw closed(); if (!isOpen) throw closed();
headers["Content-Disposition"] = headers["Content-Disposition"] =
@ -135,9 +136,9 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
headers['content-length'] = file.lengthSync().toString(); headers['content-length'] = file.lengthSync().toString();
if (streaming) { if (streaming) {
await file.openRead().pipe(this); file.openRead().pipe(this);
} else { } else {
buffer.add(await file.readAsBytes()); buffer.add(file.readAsBytesSync());
end(); end();
} }
} }
@ -157,8 +158,8 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
} }
/// Disposes of all resources. /// Disposes of all resources.
Future dispose() async { void dispose() {
await close(); close();
properties.clear(); properties.clear();
encoders.clear(); encoders.clear();
_buffer.clear(); _buffer.clear();
@ -197,11 +198,12 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
} }
/// Renders a view to the response stream, and closes the response. /// Renders a view to the response stream, and closes the response.
Future render(String view, [Map data]) async { Future render(String view, [Map data]) {
if (!isOpen) throw closed(); if (!isOpen) throw closed();
write(await app.viewGenerator(view, data)); write(app.viewGenerator(view, data));
headers['content-type'] = ContentType.HTML.toString(); headers['content-type'] = ContentType.HTML.toString();
end(); end();
return new Future.value();
} }
/// Redirects to user to the given URL. /// Redirects to user to the given URL.
@ -293,11 +295,11 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
} }
/// Copies a file's contents into the response buffer. /// Copies a file's contents into the response buffer.
Future sendFile(File file) async { void sendFile(File file) {
if (!isOpen) throw closed(); if (!isOpen) throw closed();
headers['content-type'] = lookupMimeType(file.path); headers['content-type'] = lookupMimeType(file.path);
buffer.add(await file.readAsBytes()); buffer.add(file.readAsBytesSync());
end(); end();
} }
@ -309,8 +311,7 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
var text = serializer(value); var text = serializer(value);
if (text.isEmpty) if (text.isEmpty) return;
return;
if (contentType is String) if (contentType is String)
headers['content-type'] = contentType; headers['content-type'] = contentType;
@ -337,10 +338,8 @@ abstract class ResponseContext implements StreamSink<List<int>>, StringSink {
(correspondingRequest.injections[Stopwatch] as Stopwatch).stop(); (correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
} }
if (correspondingRequest?.injections?.containsKey(PoolResource) == if (correspondingRequest?.injections?.containsKey(PoolResource) == true) {
true) { (correspondingRequest.injections[PoolResource] as PoolResource).release();
(correspondingRequest.injections[PoolResource] as PoolResource)
.release();
} }
} }

View file

@ -2,7 +2,6 @@ library angel_framework.http.routable;
import 'dart:async'; import 'dart:async';
import 'package:angel_route/angel_route.dart'; import 'package:angel_route/angel_route.dart';
import 'package:meta/meta.dart';
import '../util.dart'; import '../util.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'hooked_service.dart'; import 'hooked_service.dart';
@ -22,13 +21,23 @@ typedef Future RequestHandler(RequestContext req, ResponseContext res);
/// Sequentially runs a list of [handlers] of middleware, and returns early if any does not /// Sequentially runs a list of [handlers] of middleware, and returns early if any does not
/// return `true`. Works well with [Router].chain. /// return `true`. Works well with [Router].chain.
RequestMiddleware waterfall(List handlers) { RequestMiddleware waterfall(List handlers) {
return (RequestContext req, res) async { return (req, res) {
Future<bool> Function() runPipeline;
for (var handler in handlers) { for (var handler in handlers) {
var result = await req.app.executeHandler(handler, req, res); if (handler == null) break;
if (result != true) return result;
if (runPipeline == null)
runPipeline = () => req.app.executeHandler(handler, req, res);
else {
var current = runPipeline;
runPipeline = () => current().then((result) =>
!result ? result : req.app.executeHandler(handler, req, res));
}
} }
return true; runPipeline ??= () => new Future.value(true);
return runPipeline();
}; };
} }
@ -39,7 +48,7 @@ class Routable extends Router {
Routable() : super(); Routable() : super();
Future close() async { void close() {
_services.clear(); _services.clear();
configuration.clear(); configuration.clear();
requestMiddleware.clear(); requestMiddleware.clear();
@ -63,9 +72,11 @@ class Routable extends Router {
/// Assigns a middleware to a name for convenience. /// Assigns a middleware to a name for convenience.
@override @override
registerMiddleware(String name, @checked RequestHandler middleware) => registerMiddleware(String name, middleware) {
// ignore: deprecated_member_use assert(middleware is RequestMiddleware);
super.registerMiddleware(name, middleware); // ignore: deprecated_member_use
super.registerMiddleware(name, middleware);
}
/// Retrieves the service assigned to the given path. /// Retrieves the service assigned to the given path.
Service service(Pattern path) => Service service(Pattern path) =>
@ -116,7 +127,7 @@ class Routable extends Router {
final handlers = []; final handlers = [];
if (_router is AngelBase) { if (_router is AngelBase) {
handlers.add((RequestContext req, ResponseContext res) async { handlers.add((RequestContext req, ResponseContext res) {
req.app = _router; req.app = _router;
res.app = _router; res.app = _router;
return true; return true;

View file

@ -9,7 +9,6 @@ import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart'; import 'package:combinator/combinator.dart';
export 'package:container/container.dart'; export 'package:container/container.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../http/http.dart'; import '../http/http.dart';
import 'angel_base.dart'; import 'angel_base.dart';
@ -27,7 +26,7 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
typedef Future<HttpServer> ServerGenerator(address, int port); typedef Future<HttpServer> ServerGenerator(address, int port);
/// A function that configures an [Angel] server in some way. /// A function that configures an [Angel] server in some way.
typedef Future AngelConfigurer(Angel app); typedef FutureOr AngelConfigurer(Angel app);
/// A powerful real-time/REST/MVC server class. /// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase { class Angel extends AngelBase {
@ -202,12 +201,12 @@ class Angel extends AngelBase {
/// Shuts down the server, and closes any open [StreamController]s. /// Shuts down the server, and closes any open [StreamController]s.
/// ///
/// The server will be **COMPLETE DEFUNCT** after this operation! /// The server will be **COMPLETE DEFUNCT** after this operation!
Future close() async { Future close() {
await Future.forEach(services.values, (Service service) async { Future.forEach(services.values, (Service service) {
await service.close(); service.close();
}); });
await super.close(); super.close();
_preContained.clear(); _preContained.clear();
handlerCache.clear(); handlerCache.clear();
_injections.clear(); _injections.clear();
@ -220,8 +219,8 @@ class Angel extends AngelBase {
shutdownHooks.clear(); shutdownHooks.clear();
responseFinalizers.clear(); responseFinalizers.clear();
_flattened = null; _flattened = null;
await _http?.close(); _http?.close();
return _http?.httpServer; return new Future.value(_http?.httpServer);
} }
@override @override
@ -269,52 +268,50 @@ class Angel extends AngelBase {
this.serializer = serializer; this.serializer = serializer;
} }
Future getHandlerResult( Future getHandlerResult(handler, RequestContext req, ResponseContext res) {
handler, RequestContext req, ResponseContext res) async {
if (handler is RequestHandler) { if (handler is RequestHandler) {
var result = await handler(req, res); var result = handler(req, res);
return await getHandlerResult(result, req, res); return getHandlerResult(result, req, res);
} }
if (handler is Future) { if (handler is Future) {
var result = await handler; return handler.then((result) => getHandlerResult(result, req, res));
return await getHandlerResult(result, req, res);
} }
if (handler is Function) { if (handler is Function) {
var result = await runContained(handler, req, res); var result = runContained(handler, req, res);
return await getHandlerResult(result, req, res); return getHandlerResult(result, req, res);
} }
if (handler is Stream) { if (handler is Stream) {
return await getHandlerResult(await handler.toList(), req, res); return getHandlerResult(handler.toList(), req, res);
} }
var middleware = (req.app ?? this).findMiddleware(handler); var middleware = (req.app ?? this).findMiddleware(handler);
if (middleware != null) { if (middleware != null) {
return await getHandlerResult(middleware, req, res); return getHandlerResult(middleware, req, res);
} }
return handler; return new Future.value(handler);
} }
/// Runs some [handler]. Returns `true` if request execution should continue. /// Runs some [handler]. Returns `true` if request execution should continue.
Future<bool> executeHandler( Future<bool> executeHandler(
handler, RequestContext req, ResponseContext res) async { handler, RequestContext req, ResponseContext res) {
var result = await getHandlerResult(handler, req, res); return getHandlerResult(handler, req, res).then((result) {
if (result == null)
if (result == null) return false;
return false; else if (result is bool) {
else if (result is bool) { return result;
return result; } else if (result != null) {
} else if (result != null) { // TODO: Make `serialize` return a bool, return this as the value.
// TODO: Make `serialize` return a bool, return this as the value. // Do this wherever applicable
// Do this wherever applicable res.serialize(result,
res.serialize(result, contentType: res.headers['content-type'] ?? 'application/json');
contentType: res.headers['content-type'] ?? 'application/json'); return false;
return false; } else
} else return res.isOpen;
return res.isOpen; });
} }
/// Use the serving methods in [AngelHttp] instead. /// Use the serving methods in [AngelHttp] instead.
@ -400,20 +397,22 @@ class Angel extends AngelBase {
/// the execution will be faster, as the injection requirements were stored beforehand. /// the execution will be faster, as the injection requirements were stored beforehand.
Future runContained( Future runContained(
Function handler, RequestContext req, ResponseContext res) { Function handler, RequestContext req, ResponseContext res) {
if (_preContained.containsKey(handler)) { return new Future.sync(() {
return handleContained(handler, _preContained[handler])(req, res); if (_preContained.containsKey(handler)) {
} return handleContained(handler, _preContained[handler])(req, res);
}
return runReflected(handler, req, res); return runReflected(handler, req, res);
});
} }
/// Runs with DI, and *always* reflects. Prefer [runContained]. /// Runs with DI, and *always* reflects. Prefer [runContained].
Future runReflected( Future runReflected(
Function handler, RequestContext req, ResponseContext res) async { Function handler, RequestContext req, ResponseContext res) {
var h = var h =
handleContained(handler, _preContained[handler] = preInject(handler)); handleContained(handler, _preContained[handler] = preInject(handler));
return await h(req, res); return new Future.sync(() => h(req, res));
// return await closureMirror.apply(args).reflectee; // return closureMirror.apply(args).reflectee;
} }
/// Use the serving methods in [AngelHttp] instead. /// Use the serving methods in [AngelHttp] instead.
@ -432,8 +431,8 @@ class Angel extends AngelBase {
} }
/// Applies an [AngelConfigurer] to this instance. /// Applies an [AngelConfigurer] to this instance.
Future configure(AngelConfigurer configurer) async { Future configure(AngelConfigurer configurer) {
await configurer(this); return new Future.sync(() => configurer(this));
} }
/// Mounts the child on this router. If [routable] is `null`, /// Mounts the child on this router. If [routable] is `null`,
@ -447,7 +446,7 @@ class Angel extends AngelBase {
/// NOTE: The above will not be properly copied if [path] is /// NOTE: The above will not be properly copied if [path] is
/// a [RegExp]. /// a [RegExp].
@override @override
use(path, [@checked Routable routable, String namespace = null]) { use(path, [Router routable, String namespace = null]) {
if (routable == null) return all('*', path); if (routable == null) return all('*', path);
var head = path.toString().replaceAll(_straySlashes, ''); var head = path.toString().replaceAll(_straySlashes, '');
@ -457,13 +456,13 @@ class Angel extends AngelBase {
_preContained.addAll(routable._preContained); _preContained.addAll(routable._preContained);
if (routable.responseFinalizers.isNotEmpty) { if (routable.responseFinalizers.isNotEmpty) {
responseFinalizers.add((req, res) async { responseFinalizers.add((req, res) {
if (req.path.replaceAll(_straySlashes, '').startsWith(head)) { if (req.path.replaceAll(_straySlashes, '').startsWith(head)) {
for (var finalizer in routable.responseFinalizers) for (var finalizer in routable.responseFinalizers)
await finalizer(req, res); finalizer(req, res);
} }
return true; return new Future.value(true);
}); });
} }
@ -493,15 +492,17 @@ class Angel extends AngelBase {
} }
@deprecated @deprecated
Future<ZoneSpecification> defaultZoneCreator(request, req, res) async { Future<ZoneSpecification> defaultZoneCreator(request, req, res) {
return new ZoneSpecification( return new Future.value(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) { new ZoneSpecification(
if (logger != null) { print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
logger.info(line); if (logger != null) {
} else { logger.info(line);
return parent.print(zone, line); } else {
} return parent.print(zone, line);
}, }
},
),
); );
} }
@ -518,9 +519,8 @@ class Angel extends AngelBase {
factory Angel.fromSecurityContext(SecurityContext context) { factory Angel.fromSecurityContext(SecurityContext context) {
var app = new Angel(); var app = new Angel();
app._http = app._http = new AngelHttp.custom(app, (address, int port) {
new AngelHttp.custom(app, (address, int port) async { return HttpServer.bindSecure(address, port, context);
return await HttpServer.bindSecure(address, port, context);
}); });
return app; return app;

View file

@ -7,6 +7,7 @@ import '../util.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'hooked_service.dart' show HookedService; import 'hooked_service.dart' show HookedService;
import 'metadata.dart'; import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart'; import 'response_context.dart';
import 'routable.dart'; import 'routable.dart';
@ -54,11 +55,14 @@ class Service extends Routable {
'token' 'token'
]; ];
/// Handlers that must run to ensure this service's functionality.
List get bootstrappers => [];
/// The [Angel] app powering this service. /// The [Angel] app powering this service.
AngelBase app; AngelBase app;
/// Closes this service, including any database connections or stream controllers. /// Closes this service, including any database connections or stream controllers.
Future close() async {} void close() {}
/// Retrieves all resources. /// Retrieves all resources.
Future index([Map params]) { Future index([Map params]) {
@ -99,18 +103,22 @@ class Service extends Routable {
} }
/// Generates RESTful routes pointing to this class's methods. /// Generates RESTful routes pointing to this class's methods.
void addRoutes() { void addRoutes([Service service]) {
_addRoutesInner(service ?? this, bootstrappers);
}
void _addRoutesInner(Service service,
List handlers) {
Map restProvider = {'provider': Providers.rest}; Map restProvider = {'provider': Providers.rest};
// Add global middleware if declared on the instance itself // Add global middleware if declared on the instance itself
Middleware before = getAnnotation(this, Middleware); Middleware before = getAnnotation(service, Middleware);
final handlers = [];
if (before != null) handlers.addAll(before.handlers); if (before != null) handlers.addAll(before.handlers);
Middleware indexMiddleware = getAnnotation(this.index, Middleware); Middleware indexMiddleware = getAnnotation(service.index, Middleware);
get('/', (req, res) async { get('/', (req, res) {
return await this.index(mergeMap([ return this.index(mergeMap([
{'query': req.query}, {'query': req.query},
restProvider, restProvider,
req.serviceParams req.serviceParams
@ -120,28 +128,33 @@ class Service extends Routable {
..addAll(handlers) ..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers)); ..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(this.create, Middleware); Middleware createMiddleware = getAnnotation(service.create, Middleware);
post('/', (req, ResponseContext res) async { post('/', (RequestContext req, ResponseContext res) {
var r = await this.create( return req.lazyBody().then((body) {
await req.lazyBody(), return this
mergeMap([ .create(
{'query': req.query}, body,
restProvider, mergeMap([
req.serviceParams {'query': req.query},
])); restProvider,
res.statusCode = 201; req.serviceParams
return r; ]))
.then((r) {
res.statusCode = 201;
return r;
});
});
}, },
middleware: [] middleware: []
..addAll(handlers) ..addAll(handlers)
..addAll( ..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers)); (createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(this.read, Middleware); Middleware readMiddleware = getAnnotation(service.read, Middleware);
get( get(
'/:id', '/:id',
(req, res) async => await this.read( (req, res) => this.read(
toId(req.params['id']), toId(req.params['id']),
mergeMap([ mergeMap([
{'query': req.query}, {'query': req.query},
@ -152,56 +165,56 @@ class Service extends Routable {
..addAll(handlers) ..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(this.modify, Middleware); Middleware modifyMiddleware = getAnnotation(service.modify, Middleware);
patch( patch(
'/:id', '/:id',
(req, res) async => await this.modify( (RequestContext req, res) => req.lazyBody().then((body) => this.modify(
toId(req.params['id']), toId(req.params['id']),
await req.lazyBody(), body,
mergeMap([ mergeMap([
{'query': req.query}, {'query': req.query},
restProvider, restProvider,
req.serviceParams req.serviceParams
])), ]))),
middleware: [] middleware: []
..addAll(handlers) ..addAll(handlers)
..addAll( ..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers)); (modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(this.update, Middleware); Middleware updateMiddleware = getAnnotation(service.update, Middleware);
post( post(
'/:id', '/:id',
(req, res) async => await this.update( (RequestContext req, res) => req.lazyBody().then((body) => this.update(
toId(req.params['id']), toId(req.params['id']),
await req.lazyBody(), body,
mergeMap([ mergeMap([
{'query': req.query}, {'query': req.query},
restProvider, restProvider,
req.serviceParams req.serviceParams
])), ]))),
middleware: [] middleware: []
..addAll(handlers) ..addAll(handlers)
..addAll( ..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers)); (updateMiddleware == null) ? [] : updateMiddleware.handlers));
put( put(
'/:id', '/:id',
(req, res) async => await this.update( (RequestContext req, res) => req.lazyBody().then((body) => this.update(
toId(req.params['id']), toId(req.params['id']),
await req.lazyBody(), body,
mergeMap([ mergeMap([
{'query': req.query}, {'query': req.query},
restProvider, restProvider,
req.serviceParams req.serviceParams
])), ]))),
middleware: [] middleware: []
..addAll(handlers) ..addAll(handlers)
..addAll( ..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers)); (updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(this.remove, Middleware); Middleware removeMiddleware = getAnnotation(service.remove, Middleware);
delete( delete(
'/', '/',
(req, res) async => await this.remove( (req, res) => this.remove(
null, null,
mergeMap([ mergeMap([
{'query': req.query}, {'query': req.query},
@ -214,7 +227,7 @@ class Service extends Routable {
(removeMiddleware == null) ? [] : removeMiddleware.handlers)); (removeMiddleware == null) ? [] : removeMiddleware.handlers));
delete( delete(
'/:id', '/:id',
(req, res) async => await this.remove( (req, res) => this.remove(
toId(req.params['id']), toId(req.params['id']),
mergeMap([ mergeMap([
{'query': req.query}, {'query': req.query},

View file

@ -1,9 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io'
show
stderr,
HttpRequest,
HttpResponse,
HttpServer,
Platform,
SecurityContext;
import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart'; import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart'; import 'package:combinator/combinator.dart';
import 'package:dart2_constant/io.dart';
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'package:pool/pool.dart'; import 'package:pool/pool.dart';
import 'package:stack_trace/stack_trace.dart'; import 'package:stack_trace/stack_trace.dart';
@ -17,6 +25,7 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// Adapts `dart:io`'s [HttpServer] to serve Angel. /// Adapts `dart:io`'s [HttpServer] to serve Angel.
class AngelHttp { class AngelHttp {
final Angel app; final Angel app;
final bool useZone;
bool _closed = false; bool _closed = false;
HttpServer _server; HttpServer _server;
Future<HttpServer> Function(dynamic, int) _serverGenerator = HttpServer.bind; Future<HttpServer> Function(dynamic, int) _serverGenerator = HttpServer.bind;
@ -24,7 +33,7 @@ class AngelHttp {
Pool _pool; Pool _pool;
AngelHttp(this.app); AngelHttp(this.app, {this.useZone: true});
/// The function used to bind this instance to an HTTP server. /// The function used to bind this instance to an HTTP server.
Future<HttpServer> Function(dynamic, int) get serverGenerator => Future<HttpServer> Function(dynamic, int) get serverGenerator =>
@ -32,15 +41,18 @@ class AngelHttp {
/// An instance mounted on a server started by the [serverGenerator]. /// An instance mounted on a server started by the [serverGenerator].
factory AngelHttp.custom( factory AngelHttp.custom(
Angel app, Future<HttpServer> Function(dynamic, int) serverGenerator) { Angel app, Future<HttpServer> Function(dynamic, int) serverGenerator,
return new AngelHttp(app).._serverGenerator = serverGenerator; {bool useZone: true}) {
return new AngelHttp(app, useZone: useZone)
.._serverGenerator = serverGenerator;
} }
factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context) { factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context,
var http = new AngelHttp(app); {bool useZone: true}) {
var http = new AngelHttp(app, useZone: useZone);
http._serverGenerator = (address, int port) async { http._serverGenerator = (address, int port) {
return await HttpServer.bindSecure(address, port, context); return HttpServer.bindSecure(address, port, context);
}; };
return http; return http;
@ -53,7 +65,7 @@ class AngelHttp {
/// the server. /// the server.
factory AngelHttp.secure( factory AngelHttp.secure(
Angel app, String certificateChainPath, String serverKeyPath, Angel app, String certificateChainPath, String serverKeyPath,
{bool debug: false, String password}) { {bool debug: false, String password, bool useZone: true}) {
var certificateChain = var certificateChain =
Platform.script.resolve(certificateChainPath).toFilePath(); Platform.script.resolve(certificateChainPath).toFilePath();
var serverKey = Platform.script.resolve(serverKeyPath).toFilePath(); var serverKey = Platform.script.resolve(serverKeyPath).toFilePath();
@ -61,7 +73,8 @@ class AngelHttp {
serverContext.useCertificateChain(certificateChain, password: password); serverContext.useCertificateChain(certificateChain, password: password);
serverContext.usePrivateKey(serverKey, password: password); serverContext.usePrivateKey(serverKey, password: password);
return new AngelHttp.fromSecurityContext(app, serverContext); return new AngelHttp.fromSecurityContext(app, serverContext,
useZone: useZone);
} }
/// The native HttpServer running this instance. /// The native HttpServer running this instance.
@ -70,139 +83,153 @@ class AngelHttp {
/// Starts the server. /// Starts the server.
/// ///
/// Returns false on failure; otherwise, returns the HttpServer. /// Returns false on failure; otherwise, returns the HttpServer.
Future<HttpServer> startServer([address, int port]) async { Future<HttpServer> startServer([address, int port]) {
var host = address ?? InternetAddress.LOOPBACK_IP_V4; var host = address ?? '127.0.0.1';
_server = await _serverGenerator(host, port ?? 0); return _serverGenerator(host, port ?? 0).then((server) {
_server = server;
for (var configurer in app.startupHooks) { return Future.wait(app.startupHooks.map(app.configure)).then((_) {
await app.configure(configurer); app.optimizeForProduction();
} _sub = _server.listen(handleRequest);
return _server;
app.optimizeForProduction(); });
_sub = _server.listen(handleRequest); });
return _server;
} }
/// Shuts down the underlying server. /// Shuts down the underlying server.
Future<HttpServer> close() async { Future<HttpServer> close() {
if (_closed) return _server; if (_closed) return new Future.value(_server);
_closed = true; _closed = true;
_sub?.cancel(); _sub?.cancel();
// TODO: Remove this try/catch in 1.2.0 // TODO: Remove this try/catch in 1.2.0
try { var close = app.close().catchError((_) => null);
await app.close(); return close.then((_) =>
} catch (_) {} Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _server));
for (var configurer in app.shutdownHooks) await app.configure(configurer);
return _server;
} }
/// Handles a single request. /// Handles a single request.
Future handleRequest(HttpRequest request) async { Future handleRequest(HttpRequest request) {
var req = await createRequestContext(request); return createRequestContext(request).then((req) {
var res = await createResponseContext(request.response, req); return createResponseContext(request.response, req).then((res) {
handle() {
var path = req.path;
if (path == '/') path = '';
var zoneSpec = new ZoneSpecification( Tuple3<List, Map, ParseResult<Map<String, String>>> resolveTuple() {
print: (self, parent, zone, line) { Router r = app.optimizedRouter;
if (app.logger != null) var resolved =
app.logger.info(line); r.resolveAbsolute(path, method: req.method, strip: false);
else
parent.print(zone, line);
},
handleUncaughtError: (self, parent, zone, error, stackTrace) async {
var trace = new Trace.from(stackTrace).terse;
try { return new Tuple3(
AngelHttpException e; new MiddlewarePipeline(resolved).handlers,
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)),
if (error is FormatException) { resolved.isEmpty ? null : resolved.first.parseResult,
e = new AngelHttpException.badRequest(message: error.message); );
} else if (error is AngelHttpException) {
e = error;
} else {
e = new AngelHttpException(error,
stackTrace: stackTrace, message: error?.toString());
} }
if (app.logger != null) { var cacheKey = req.method + path;
app.logger.severe(e.message ?? e.toString(), error, trace); var tuple = app.isProduction
? app.handlerCache.putIfAbsent(cacheKey, resolveTuple)
: resolveTuple();
req.params.addAll(tuple.item2);
req.inject(ParseResult, tuple.item3);
if (app.logger != null)
req.inject(Stopwatch, new Stopwatch()..start());
var pipeline = tuple.item1;
Future<bool> Function() runPipeline;
for (var handler in pipeline) {
if (handler == null) break;
if (runPipeline == null)
runPipeline = () => app.executeHandler(handler, req, res);
else {
var current = runPipeline;
runPipeline = () => current().then((result) =>
!result ? result : app.executeHandler(handler, req, res));
}
} }
await handleAngelHttpException(e, trace, req, res, request); return runPipeline == null
} catch (e, st) { ? sendResponse(request, req, res)
var trace = new Trace.from(st).terse; : runPipeline().then((_) => sendResponse(request, req, res));
request.response.close();
// Ideally, we won't be in a position where an absolutely fatal error occurs,
// but if so, we'll need to log it.
if (app.logger != null) {
app.logger.severe(
'Fatal error occurred when processing ${request.uri}.',
e,
trace);
} else {
stderr
..writeln('Fatal error occurred when processing '
'${request.uri}:')
..writeln(e)
..writeln(trace);
}
} }
},
);
var zone = Zone.current.fork(specification: zoneSpec); if (useZone == false) {
return handle().whenComplete(() => res.dispose());
} else {
var zoneSpec = new ZoneSpecification(
print: (self, parent, zone, line) {
if (app.logger != null)
app.logger.info(line);
else
parent.print(zone, line);
},
handleUncaughtError: (self, parent, zone, error, stackTrace) {
var trace = new Trace.from(stackTrace).terse;
return await zone.run(() async { return new Future(() {
var path = req.path; AngelHttpException e;
if (path == '/') path = '';
Tuple3<List, Map, ParseResult<Map<String, String>>> resolveTuple() { if (error is FormatException) {
Router r = app.optimizedRouter; e = new AngelHttpException.badRequest(message: error.message);
var resolved = } else if (error is AngelHttpException) {
r.resolveAbsolute(path, method: req.method, strip: false); e = error;
} else {
e = new AngelHttpException(error,
stackTrace: stackTrace, message: error?.toString());
}
return new Tuple3( if (app.logger != null) {
new MiddlewarePipeline(resolved).handlers, app.logger.severe(e.message ?? e.toString(), error, trace);
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)), }
resolved.isEmpty ? null : resolved.first.parseResult,
);
}
var cacheKey = req.method + path; return handleAngelHttpException(e, trace, req, res, request);
var tuple = app.isProduction }).catchError((e, st) {
? app.handlerCache.putIfAbsent(cacheKey, resolveTuple) var trace = new Trace.from(st).terse;
: resolveTuple(); request.response.close();
// Ideally, we won't be in a position where an absolutely fatal error occurs,
// but if so, we'll need to log it.
if (app.logger != null) {
app.logger.severe(
'Fatal error occurred when processing ${request.uri}.',
e,
trace);
} else {
stderr
..writeln('Fatal error occurred when processing '
'${request.uri}:')
..writeln(e)
..writeln(trace);
}
});
},
);
req.inject(Zone, zone); var zone = Zone.current.fork(specification: zoneSpec);
req.inject(ZoneSpecification, zoneSpec); req.inject(Zone, zone);
req.params.addAll(tuple.item2); req.inject(ZoneSpecification, zoneSpec);
req.inject(ParseResult, tuple.item3); return zone.run(handle).whenComplete(() {
res.dispose();
if (app.logger != null) req.inject(Stopwatch, new Stopwatch()..start()); });
}
var pipeline = tuple.item1; });
for (var handler in pipeline) {
if (handler == null || !await app.executeHandler(handler, req, res))
break;
}
await sendResponse(request, req, res);
}).whenComplete(() {
res.dispose();
}); });
} }
/// Handles an [AngelHttpException]. /// Handles an [AngelHttpException].
Future handleAngelHttpException(AngelHttpException e, StackTrace st, Future handleAngelHttpException(AngelHttpException e, StackTrace st,
RequestContext req, ResponseContext res, HttpRequest request, RequestContext req, ResponseContext res, HttpRequest request,
{bool ignoreFinalizers: false}) async { {bool ignoreFinalizers: false}) {
if (req == null || res == null) { if (req == null || res == null) {
try { try {
app.logger?.severe(e, st); app.logger?.severe(e, st);
request.response request.response
..statusCode = HttpStatus.INTERNAL_SERVER_ERROR ..statusCode = HttpStatus.internalServerError
..write('500 Internal Server Error') ..write('500 Internal Server Error')
..close(); ..close();
} finally { } finally {
@ -212,12 +239,12 @@ class AngelHttp {
if (res.isOpen) { if (res.isOpen) {
res.statusCode = e.statusCode; res.statusCode = e.statusCode;
var result = await app.errorHandler(e, req, res); var result = app.errorHandler(e, req, res);
await app.executeHandler(result, req, res); app.executeHandler(result, req, res);
res.end(); res.end();
} }
return await sendResponse(request, req, res, return sendResponse(request, req, res,
ignoreFinalizers: ignoreFinalizers == true); ignoreFinalizers: ignoreFinalizers == true);
} }
@ -244,8 +271,12 @@ class AngelHttp {
List<int> outputBuffer = res.buffer.toBytes(); List<int> outputBuffer = res.buffer.toBytes();
if (res.encoders.isNotEmpty) { if (res.encoders.isNotEmpty) {
var allowedEncodings = var allowedEncodings = req.headers
req.headers[HttpHeaders.ACCEPT_ENCODING]?.map((str) { .value('accept-encoding')
?.split(',')
?.map((s) => s.trim())
?.where((s) => s.isNotEmpty)
?.map((str) {
// Ignore quality specifications in accept-encoding // Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8 // ex. gzip;q=0.8
if (!str.contains(';')) return str; if (!str.contains(';')) return str;
@ -264,7 +295,7 @@ class AngelHttp {
} }
if (encoder != null) { if (encoder != null) {
request.response.headers.set(HttpHeaders.CONTENT_ENCODING, key); request.response.headers.set('content-encoding', key);
outputBuffer = res.encoders[key].convert(outputBuffer); outputBuffer = res.encoders[key].convert(outputBuffer);
request.response.contentLength = outputBuffer.length; request.response.contentLength = outputBuffer.length;
break; break;
@ -278,31 +309,30 @@ class AngelHttp {
..cookies.addAll(res.cookies) ..cookies.addAll(res.cookies)
..add(outputBuffer); ..add(outputBuffer);
return finalizers.then((_) async { return finalizers.then((_) {
await request.response.flush(); return request.response.close().then((_) {
await request.response.close(); if (req.injections.containsKey(PoolResource)) {
req.injections[PoolResource].release();
if (req.injections.containsKey(PoolResource)) {
req.injections[PoolResource].release();
}
if (app.logger != null) {
var sw = req.grab<Stopwatch>(Stopwatch);
if (sw.isRunning) {
sw?.stop();
app.logger.info("${res.statusCode} ${req.method} ${req.uri} (${sw
?.elapsedMilliseconds ?? 'unknown'} ms)");
} }
}
if (app.logger != null) {
var sw = req.grab<Stopwatch>(Stopwatch);
if (sw.isRunning) {
sw?.stop();
app.logger.info("${res.statusCode} ${req.method} ${req.uri} (${sw
?.elapsedMilliseconds ?? 'unknown'} ms)");
}
}
});
}); });
} }
Future<HttpRequestContextImpl> createRequestContext(HttpRequest request) { Future<HttpRequestContextImpl> createRequestContext(HttpRequest request) {
var path = request.uri.path.replaceAll(_straySlashes, ''); var path = request.uri.path.replaceAll(_straySlashes, '');
if (path.length == 0) path = '/'; if (path.length == 0) path = '/';
return HttpRequestContextImpl.from(request, app, path).then((req) async { return HttpRequestContextImpl.from(request, app, path).then((req) {
if (_pool != null) req.inject(PoolResource, await _pool.request()); if (_pool != null) req.inject(PoolResource, _pool.request());
if (app.injections.isNotEmpty) app.injections.forEach(req.inject); if (app.injections.isNotEmpty) app.injections.forEach(req.inject);
return req; return req;
}); });

View file

@ -27,7 +27,7 @@ class Controller {
Controller({this.debug: false, this.injectSingleton: true}); Controller({this.debug: false, this.injectSingleton: true});
@mustCallSuper @mustCallSuper
Future configureServer(Angel app) async { Future configureServer(Angel app) {
_app = app; _app = app;
if (injectSingleton != false) _app.container.singleton(this); if (injectSingleton != false) _app.container.singleton(this);
@ -56,6 +56,7 @@ class Controller {
final routeBuilder = _routeBuilder(instanceMirror, routable, handlers); final routeBuilder = _routeBuilder(instanceMirror, routable, handlers);
classMirror.instanceMembers.forEach(routeBuilder); classMirror.instanceMembers.forEach(routeBuilder);
configureRoutes(routable); configureRoutes(routable);
return new Future.value();
} }
Function _routeBuilder( Function _routeBuilder(
@ -85,9 +86,9 @@ class Controller {
method.parameters[1].type.reflectedType == ResponseContext) { method.parameters[1].type.reflectedType == ResponseContext) {
// Create a regular route // Create a regular route
routeMappings[name] = routable routeMappings[name] = routable
.addRoute(exposeDecl.method, exposeDecl.path, (req, res) async { .addRoute(exposeDecl.method, exposeDecl.path, (req, res) {
var result = await reflectedMethod(req, res); var result = reflectedMethod(req, res);
return result is RequestHandler ? await result(req, res) : result; return result is RequestHandler ? result(req, res) : result;
}, middleware: middleware); }, middleware: middleware);
return; return;
} }

View file

@ -14,10 +14,10 @@ export 'http_response_context.dart';
/// Boots a shared server instance. Use this if launching multiple isolates /// Boots a shared server instance. Use this if launching multiple isolates
Future<HttpServer> startShared(address, int port) => HttpServer Future<HttpServer> startShared(address, int port) => HttpServer
.bind(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0, shared: true); .bind(address ?? '127.0.0.1', port ?? 0, shared: true);
Future<HttpServer> Function(dynamic, int) startSharedSecure(SecurityContext securityContext) { Future<HttpServer> Function(dynamic, int) startSharedSecure(SecurityContext securityContext) {
return (address, int port) => HttpServer.bindSecure( return (address, int port) => HttpServer.bindSecure(
address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0, securityContext, address ?? '127.0.0.1', port ?? 0, securityContext,
shared: true); shared: true);
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:body_parser/body_parser.dart'; import 'package:body_parser/body_parser.dart';
import 'package:http_parser/http_parser.dart';
import '../core/core.dart'; import '../core/core.dart';
/// An implementation of [RequestContext] that wraps a [HttpRequest]. /// An implementation of [RequestContext] that wraps a [HttpRequest].
@ -34,7 +35,7 @@ class HttpRequestContextImpl extends RequestContext {
@override @override
String get method { String get method {
return _override ?? originalMethod; return _override ?? originalMethod;
} }
@override @override
@ -70,7 +71,7 @@ class HttpRequestContextImpl extends RequestContext {
/// Magically transforms an [HttpRequest] into a [RequestContext]. /// Magically transforms an [HttpRequest] into a [RequestContext].
static Future<HttpRequestContextImpl> from( static Future<HttpRequestContextImpl> from(
HttpRequest request, Angel app, String path) async { HttpRequest request, Angel app, String path) {
HttpRequestContextImpl ctx = new HttpRequestContextImpl(); HttpRequestContextImpl ctx = new HttpRequestContextImpl();
String override = request.method; String override = request.method;
@ -116,10 +117,10 @@ class HttpRequestContextImpl extends RequestContext {
ctx._io = request; ctx._io = request;
if (app.lazyParseBodies != true) { if (app.lazyParseBodies != true) {
await ctx.parse(); return ctx.parse().then((_) => ctx);
} }
return ctx; return new Future.value(ctx);
} }
@override @override
@ -131,8 +132,13 @@ class HttpRequestContextImpl extends RequestContext {
} }
@override @override
Future<BodyParseResult> parseOnce() async { Future<BodyParseResult> parseOnce() {
return await parseBody(io, return parseBodyFromStream(
io,
io.headers.contentType != null
? new MediaType.parse(io.headers.contentType.toString())
: null,
io.uri,
storeOriginalBuffer: app.storeOriginalBuffer == true); storeOriginalBuffer: app.storeOriginalBuffer == true);
} }
} }

View file

@ -111,10 +111,10 @@ class HttpResponseContextImpl extends ResponseContext {
} }
@override @override
Future close() async { Future close() {
if (_useStream) { if (_useStream) {
try { try {
await io.close(); io.close();
} catch(_) { } catch(_) {
// This only seems to occur on `MockHttpRequest`, but // This only seems to occur on `MockHttpRequest`, but
// this try/catch prevents a crash. // this try/catch prevents a crash.
@ -122,7 +122,8 @@ class HttpResponseContextImpl extends ResponseContext {
} }
_isClosed = true; _isClosed = true;
await super.close(); super.close();
_useStream = false; _useStream = false;
return new Future.value();
} }
} }

View file

@ -58,10 +58,10 @@ class AngelMetrics extends Angel {
final AngelMetricsStats stats = new AngelMetricsStats._(); final AngelMetricsStats stats = new AngelMetricsStats._();
@override @override
Future<HttpServer> close() async { Future<HttpServer> close() {
_sub?.cancel(); _sub?.cancel();
await _inner.close(); _inner.close();
return await super.close(); return super.close();
} }
@override @override

View file

@ -23,13 +23,14 @@ main() async {
void start(int id) { void start(int id) {
var app = new Angel()..lazyParseBodies = true; var app = new Angel()..lazyParseBodies = true;
var http = new AngelHttp.custom(app, startShared); var http = new AngelHttp.custom(app, startShared, useZone: false);
app.get('/', (req, ResponseContext res) { app.get('/', (req, ResponseContext res) {
res.willCloseItself = true; res.write('Hello, world!');
res.io //res.willCloseItself = true;
..write('Hello, world!') //res.io
..close(); // ..write('Hello, world!')
// ..close();
return false; return false;
}); });
@ -41,7 +42,7 @@ void start(int id) {
return oldHandler(e, req, res); return oldHandler(e, req, res);
}; };
http.startServer(InternetAddress.LOOPBACK_IP_V4, 3000).then((server) { http.startServer('127.0.0.1', 3000).then((server) {
print( print(
'Instance #$id listening at http://${server.address.address}:${server.port}'); 'Instance #$id listening at http://${server.address.address}:${server.port}');
}); });

View file

@ -12,7 +12,7 @@ main() {
void start(int id) { void start(int id) {
HttpServer HttpServer
.bind(InternetAddress.LOOPBACK_IP_V4, 3000, shared: true) .bind('127.0.0.1', 3000, shared: true)
.then((server) { .then((server) {
print( print(
'Instance #$id listening at http://${server.address.address}:${server.port}'); 'Instance #$id listening at http://${server.address.address}:${server.port}');

View file

@ -1,5 +1,5 @@
name: angel_framework name: angel_framework
version: 1.1.3 version: 1.1.4
description: A high-powered HTTP server with DI, routing and more. description: A high-powered HTTP server with DI, routing and more.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_framework homepage: https://github.com/angel-dart/angel_framework
@ -14,6 +14,7 @@ dependencies:
combinator: ">=1.0.0-beta <2.0.0" combinator: ">=1.0.0-beta <2.0.0"
container: ^0.1.2 container: ^0.1.2
dart2_constant: ^1.0.0 dart2_constant: ^1.0.0
http_parser: ^3.0.0
json_god: ^2.0.0-beta json_god: ^2.0.0-beta
logging: ">=0.11.3 <1.0.0" logging: ">=0.11.3 <1.0.0"
matcher: ^0.12.0 matcher: ^0.12.0
@ -23,6 +24,7 @@ dependencies:
path: ">=1.0.0 <2.0.0" path: ">=1.0.0 <2.0.0"
pool: ^1.0.0 pool: ^1.0.0
random_string: ^0.0.1 random_string: ^0.0.1
stack_trace: ^1.0.0
tuple: ^1.0.0 tuple: ^1.0.0
dev_dependencies: dev_dependencies:
mock_request: ^1.0.0 mock_request: ^1.0.0

View file

@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io' show BytesBuilder;
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart'; import 'package:dart2_constant/convert.dart';
import 'package:dart2_constant/io.dart';
import 'package:mock_request/mock_request.dart'; import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -19,8 +20,8 @@ main() {
app = new Angel(); app = new Angel();
app.injectEncoders( app.injectEncoders(
{ {
'deflate': ZLIB.encoder, 'deflate': zlib.encoder,
'gzip': GZIP.encoder, 'gzip': gzip.encoder,
}, },
); );
@ -55,50 +56,50 @@ void encodingTests(Angel getApp()) {
test('encodes if wildcard', () async { test('encodes if wildcard', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/hello')) var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
..headers.set(HttpHeaders.ACCEPT_ENCODING, '*') ..headers.set('accept-encoding', '*')
..close(); ..close();
var rs = rq.response; var rs = rq.response;
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await getBody(rs); var body = await getBody(rs);
expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'deflate'); expect(rs.headers.value('content-encoding'), 'deflate');
expect(body, ZLIB.encode(utf8.encode('Hello, world!'))); expect(body, zlib.encode(utf8.encode('Hello, world!')));
}); });
test('encodes if wildcard + multiple', () async { test('encodes if wildcard + multiple', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/hello')) var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
..headers.set(HttpHeaders.ACCEPT_ENCODING, ['foo', 'bar', '*']) ..headers.set('accept-encoding', ['foo', 'bar', '*'])
..close(); ..close();
var rs = rq.response; var rs = rq.response;
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await getBody(rs); var body = await getBody(rs);
expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'deflate'); expect(rs.headers.value('content-encoding'), 'deflate');
expect(body, ZLIB.encode(utf8.encode('Hello, world!'))); expect(body, zlib.encode(utf8.encode('Hello, world!')));
}); });
test('encodes if explicit', () async { test('encodes if explicit', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/hello')) var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
..headers.set(HttpHeaders.ACCEPT_ENCODING, 'gzip'); ..headers.set('accept-encoding', 'gzip');
await rq.close(); await rq.close();
var rs = rq.response; var rs = rq.response;
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await getBody(rs); var body = await getBody(rs);
expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip'); expect(rs.headers.value('content-encoding'), 'gzip');
expect(body, GZIP.encode(utf8.encode('Hello, world!'))); expect(body, gzip.encode(utf8.encode('Hello, world!')));
}); });
test('only uses one encoder', () async { test('only uses one encoder', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/hello')) var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
..headers.set(HttpHeaders.ACCEPT_ENCODING, ['gzip', 'deflate']); ..headers.set('accept-encoding', ['gzip', 'deflate']);
await rq.close(); await rq.close();
var rs = rq.response; var rs = rq.response;
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await getBody(rs); var body = await getBody(rs);
expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip'); expect(rs.headers.value('content-encoding'), 'gzip');
expect(body, GZIP.encode(utf8.encode('Hello, world!'))); expect(body, gzip.encode(utf8.encode('Hello, world!')));
}); });
}); });
} }

View file

@ -1,8 +1,7 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:matcher/matcher.dart'; import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { main() {
@ -41,8 +40,8 @@ main() {
test('toMap = toJson', () { test('toMap = toJson', () {
var exc = new AngelHttpException.badRequest(); var exc = new AngelHttpException.badRequest();
expect(exc.toMap(), exc.toJson()); expect(exc.toMap(), exc.toJson());
var json = JSON.encode(exc.toJson()); var json_ = json.encode(exc.toJson());
var exc2 = new AngelHttpException.fromJson(json); var exc2 = new AngelHttpException.fromJson(json_);
expect(exc2.toJson(), exc.toJson()); expect(exc2.toJson(), exc.toJson());
}); });
@ -68,7 +67,9 @@ class _IsException extends Matcher {
description.add('has status code $statusCode and message "$message"'); description.add('has status code $statusCode and message "$message"');
@override @override
bool matches(@checked AngelHttpException item, Map matchState) { bool matches(item, Map matchState) {
return item.statusCode == statusCode && item.message == message; return item is AngelHttpException &&
item.statusCode == statusCode &&
item.message == message;
} }
} }

View file

@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -32,6 +32,6 @@ main() {
var response = await client var response = await client
.get('$url/foo', headers: {'X-HTTP-Method-Override': 'POST'}); .get('$url/foo', headers: {'X-HTTP-Method-Override': 'POST'});
print('Response: ${response.body}'); print('Response: ${response.body}');
expect(JSON.decode(response.body), equals({'hello': 'world'})); expect(json.decode(response.body), equals({'hello': 'world'}));
}); });
} }

View file

@ -51,12 +51,13 @@ main() {
return 'DEFAULT $mode'; return 'DEFAULT $mode';
}); });
app.logger = new Logger('parameter_meta_test') /*app.logger = new Logger('parameter_meta_test')
..onRecord.listen((rec) { ..onRecord.listen((rec) {
print(rec); print(rec);
if (rec.error != null) print(rec.error); if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace); if (rec.stackTrace != null) print(rec.stackTrace);
}); });
*/
}); });
test('injects header or throws', () async { test('injects header or throws', () async {

View file

@ -1,5 +1,5 @@
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:mock_request/mock_request.dart'; import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -16,8 +16,8 @@ main() {
rq.close(); rq.close();
await new AngelHttp(app).handleRequest(rq); await new AngelHttp(app).handleRequest(rq);
var rs = rq.response; var rs = rq.response;
var body = await rs.transform(UTF8.decoder).join(); var body = await rs.transform(utf8.decoder).join();
expect(body, JSON.encode('bar')); expect(body, json.encode('bar'));
}); });
} }

View file

@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:io' show stderr;
import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:mock_request/mock_request.dart'; import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -36,22 +36,22 @@ main() {
test('String type annotation', () async { test('String type annotation', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/string/hello'))..close(); var rq = new MockHttpRequest('GET', Uri.parse('/string/hello'))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var rs = await rq.response.transform(UTF8.decoder).join(); var rs = await rq.response.transform(utf8.decoder).join();
expect(rs, JSON.encode('hello')); expect(rs, json.encode('hello'));
}); });
test('Primitive after parsed param injection', () async { test('Primitive after parsed param injection', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/num/parsed/24'))..close(); var rq = new MockHttpRequest('GET', Uri.parse('/num/parsed/24'))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var rs = await rq.response.transform(UTF8.decoder).join(); var rs = await rq.response.transform(utf8.decoder).join();
expect(rs, JSON.encode(24)); expect(rs, json.encode(24));
}); });
test('globally-injected primitive', () async { test('globally-injected primitive', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/num/global'))..close(); var rq = new MockHttpRequest('GET', Uri.parse('/num/global'))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var rs = await rq.response.transform(UTF8.decoder).join(); var rs = await rq.response.transform(utf8.decoder).join();
expect(rs, JSON.encode(305)); expect(rs, json.encode(305));
}); });
test('unparsed primitive throws error', () async { test('unparsed primitive throws error', () async {

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:mock_request/mock_request.dart'; import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -13,9 +13,9 @@ main() {
var app = new Angel()..get('/test/:id', (id) => 'Hello $id'); var app = new Angel()..get('/test/:id', (id) => 'Hello $id');
var rq1 = mk(1), rq2 = mk(2), rq3 = mk(1); var rq1 = mk(1), rq2 = mk(2), rq3 = mk(1);
await Future.wait([rq1, rq2, rq3].map(new AngelHttp(app).handleRequest)); await Future.wait([rq1, rq2, rq3].map(new AngelHttp(app).handleRequest));
var body1 = await rq1.response.transform(UTF8.decoder).join(), var body1 = await rq1.response.transform(utf8.decoder).join(),
body2 = await rq2.response.transform(UTF8.decoder).join(), body2 = await rq2.response.transform(utf8.decoder).join(),
body3 = await rq3.response.transform(UTF8.decoder).join(); body3 = await rq3.response.transform(utf8.decoder).join();
print('Response #1: $body1'); print('Response #1: $body1');
print('Response #2: $body2'); print('Response #2: $body2');
print('Response #3: $body3'); print('Response #3: $body3');

View file

@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -96,7 +95,7 @@ main() {
client = new http.Client(); client = new http.Client();
var server = var server =
await new AngelHttp(app).startServer(InternetAddress.LOOPBACK_IP_V4, 0); await new AngelHttp(app).startServer('127.0.0.1', 0);
url = "http://${server.address.host}:${server.port}"; url = "http://${server.address.host}:${server.port}";
}); });
@ -118,10 +117,10 @@ main() {
test('Can match url with multiple parameters', () async { test('Can match url with multiple parameters', () async {
var response = await client.get('$url/name/HELLO/last/WORLD'); var response = await client.get('$url/name/HELLO/last/WORLD');
print('Response: ${response.body}'); print('Response: ${response.body}');
var json = JSON.decode(response.body); var json_ = json.decode(response.body);
expect(json, new isInstanceOf<Map<String, String>>()); expect(json_, new isInstanceOf<Map<String, String>>());
expect(json['first'], equals('HELLO')); expect(json_['first'], equals('HELLO'));
expect(json['last'], equals('WORLD')); expect(json_['last'], equals('WORLD'));
}); });
test('Chained routes', () async { test('Chained routes', () async {
@ -131,16 +130,16 @@ main() {
test('Can nest another Angel instance', () async { test('Can nest another Angel instance', () async {
var response = await client.post('$url/nes/ted/foo'); var response = await client.post('$url/nes/ted/foo');
var json = JSON.decode(response.body); var json_ = json.decode(response.body);
expect(json['route'], equals('foo')); expect(json_['route'], equals('foo'));
}); });
test('Can parse parameters from a nested Angel instance', () async { test('Can parse parameters from a nested Angel instance', () async {
var response = await client.get('$url/todos/1337/action/test'); var response = await client.get('$url/todos/1337/action/test');
var json = JSON.decode(response.body); var json_ = json.decode(response.body);
print('JSON: $json'); print('JSON: $json_');
expect(json['id'], equals('1337')); expect(json_['id'], equals('1337'));
expect(json['action'], equals('test')); expect(json_['action'], equals('test'));
}); });
test('Can add and use named middleware', () async { test('Can add and use named middleware', () async {
@ -156,10 +155,11 @@ main() {
test('Can serialize function result as JSON', () async { test('Can serialize function result as JSON', () async {
Map headers = {'Content-Type': 'application/json'}; Map headers = {'Content-Type': 'application/json'};
String postData = JSON.encode({'it': 'works'}); String postData = json.encode({'it': 'works'});
var response = var response =
await client.post("$url/lambda", headers: headers, body: postData); await client.post("$url/lambda", headers: headers, body: postData);
expect(JSON.decode(response.body)['it'], equals('works')); print('Response: ${response.body}');
expect(json.decode(response.body)['it'], equals('works'));
}); });
test('Fallback routes', () async { test('Fallback routes', () async {
@ -178,14 +178,14 @@ main() {
test('Redirect to named routes', () async { test('Redirect to named routes', () async {
var response = await client.get('$url/named'); var response = await client.get('$url/named');
print(response.body); print(response.body);
expect(JSON.decode(response.body), equals('Hello tests')); expect(json.decode(response.body), equals('Hello tests'));
}); });
test('Match routes, even with query params', () async { test('Match routes, even with query params', () async {
var response = var response =
await client.get("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo"); await client.get("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo");
print(response.body); print(response.body);
expect(JSON.decode(response.body), equals('Logged')); expect(json.decode(response.body), equals('Logged'));
response = await client.get("$url/query/foo?bar=baz"); response = await client.get("$url/query/foo?bar=baz");
print(response.body); print(response.body);

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:matcher/matcher.dart'; import 'package:matcher/matcher.dart';
import 'package:mock_request/mock_request.dart'; import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -51,7 +51,7 @@ main() {
ContentType.HTML.mimeType, ContentType.HTML.mimeType,
); );
expect(rs.statusCode, e.statusCode); expect(rs.statusCode, e.statusCode);
var body = await rs.transform(UTF8.decoder).join(); var body = await rs.transform(utf8.decoder).join();
expect(body, contains('<title>${e.message}</title>')); expect(body, contains('<title>${e.message}</title>'));
expect(body, contains('<li>foo</li>')); expect(body, contains('<li>foo</li>'));
expect(body, contains('<li>bar</li>')); expect(body, contains('<li>bar</li>'));
@ -59,9 +59,7 @@ main() {
test('plug-ins run on startup', () async { test('plug-ins run on startup', () async {
var app = new Angel(); var app = new Angel();
app.startupHooks.add((app) async { app.startupHooks.add((app) => app.configuration['two'] = 2);
app.configuration['two'] = 2;
});
var http = new AngelHttp(app); var http = new AngelHttp(app);
await http.startServer(); await http.startServer();
@ -92,8 +90,8 @@ main() {
app.get('/', (String a) => a); app.get('/', (String a) => a);
var rq = new MockHttpRequest('GET', Uri.parse('/'))..close(); var rq = new MockHttpRequest('GET', Uri.parse('/'))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await rq.response.transform(UTF8.decoder).join(); var body = await rq.response.transform(utf8.decoder).join();
expect(body, JSON.encode('b')); expect(body, json.encode('b'));
}); });
test('global injected serializer', () async { test('global injected serializer', () async {
@ -102,7 +100,7 @@ main() {
app.get($foo.path, (req, ResponseContext res) => res.serialize(null)); app.get($foo.path, (req, ResponseContext res) => res.serialize(null));
var rq = new MockHttpRequest('GET', $foo)..close(); var rq = new MockHttpRequest('GET', $foo)..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await rq.response.transform(UTF8.decoder).join(); var body = await rq.response.transform(utf8.decoder).join();
expect(body, 'x'); expect(body, 'x');
}); });
@ -157,7 +155,7 @@ main() {
app.get('/wtf', () => throw new AngelHttpException.forbidden()); app.get('/wtf', () => throw new AngelHttpException.forbidden());
app.get('/wtf2', () => throw new AngelHttpException.forbidden()); app.get('/wtf2', () => throw new AngelHttpException.forbidden());
http = new AngelHttp(app); http = new AngelHttp(app);
await http.startServer(InternetAddress.LOOPBACK_IP_V4, 0); await http.startServer('127.0.0.1', 0);
var oldHandler = app.errorHandler; var oldHandler = app.errorHandler;
app.errorHandler = (e, req, res) { app.errorHandler = (e, req, res) {
@ -208,9 +206,9 @@ class CustomCloseService extends Service {
int value = 2; int value = 2;
@override @override
Future close() { void close() {
value = 3; value = 3;
return super.close(); super.close();
} }
} }

View file

@ -1,7 +1,8 @@
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'package:stack_trace/stack_trace.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class Todo extends Model { class Todo extends Model {
@ -23,7 +24,7 @@ main() {
..use('/todos', new TypedService<Todo>(new MapService())) ..use('/todos', new TypedService<Todo>(new MapService()))
..errorHandler = (e, req, res) { ..errorHandler = (e, req, res) {
print('Whoops: ${e.error}'); print('Whoops: ${e.error}');
print(e.stackTrace); if (e.stackTrace != null) print(new Chain.forTrace(e.stackTrace).terse);
}; };
var server = await new AngelHttp(app).startServer(); var server = await new AngelHttp(app).startServer();
@ -45,7 +46,7 @@ main() {
print(response.body); print(response.body);
expect(response.body, equals('[]')); expect(response.body, equals('[]'));
print(response.body); print(response.body);
expect(JSON.decode(response.body).length, 0); expect(json.decode(response.body).length, 0);
}); });
test('can create data', () async { test('can create data', () async {
@ -97,12 +98,12 @@ main() {
String postData = god.serialize({'text': 'Hello, world!'}); String postData = god.serialize({'text': 'Hello, world!'});
var created = await client var created = await client
.post("$url/todos", headers: headers, body: postData) .post("$url/todos", headers: headers, body: postData)
.then((r) => JSON.decode(r.body)); .then((r) => json.decode(r.body));
var response = await client.delete("$url/todos/${created['id']}"); var response = await client.delete("$url/todos/${created['id']}");
expect(response.statusCode, 200); expect(response.statusCode, 200);
var json = god.deserialize(response.body); var json_ = god.deserialize(response.body);
print(json); print(json_);
expect(json['text'], equals('Hello, world!')); expect(json_['text'], equals('Hello, world!'));
}); });
}); });
} }

View file

@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:io' show stderr;
import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:dart2_constant/convert.dart';
import 'package:dart2_constant/io.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:mock_request/mock_request.dart'; import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -23,8 +24,8 @@ main() {
app.injectEncoders( app.injectEncoders(
{ {
'deflate': ZLIB.encoder, 'deflate': zlib.encoder,
'gzip': GZIP.encoder, 'gzip': gzip.encoder,
}, },
); );
@ -73,7 +74,7 @@ main() {
_expectHelloBye(String path) async { _expectHelloBye(String path) async {
var rq = new MockHttpRequest('GET', Uri.parse(path))..close(); var rq = new MockHttpRequest('GET', Uri.parse(path))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await rq.response.transform(UTF8.decoder).join(); var body = await rq.response.transform(utf8.decoder).join();
expect(body, 'Hello, world!bye'); expect(body, 'Hello, world!bye');
} }
@ -84,7 +85,7 @@ main() {
test('cannot write after close', () async { test('cannot write after close', () async {
var rq = new MockHttpRequest('GET', Uri.parse('/overwrite'))..close(); var rq = new MockHttpRequest('GET', Uri.parse('/overwrite'))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await rq.response.transform(UTF8.decoder).join(); var body = await rq.response.transform(utf8.decoder).join();
if (rq.response.statusCode != 32) if (rq.response.statusCode != 32)
throw 'overwrite should throw error; response: $body'; throw 'overwrite should throw error; response: $body';
@ -94,7 +95,7 @@ main() {
try { try {
var rq = new MockHttpRequest('GET', Uri.parse('/error'))..close(); var rq = new MockHttpRequest('GET', Uri.parse('/error'))..close();
await http.handleRequest(rq); await http.handleRequest(rq);
var body = await rq.response.transform(UTF8.decoder).join(); var body = await rq.response.transform(utf8.decoder).join();
throw 'addError should throw error; response: $body'; throw 'addError should throw error; response: $body';
} on StateError { } on StateError {
// Should throw error... // Should throw error...