diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 06615469..264fd455 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -19,7 +19,7 @@ - @@ -399,7 +399,7 @@ - + diff --git a/.idea/runConfigurations/performance__hello.xml b/.idea/runConfigurations/performance__hello.xml new file mode 100644 index 00000000..910062a6 --- /dev/null +++ b/.idea/runConfigurations/performance__hello.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 0aa163ff..b7eb8194 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,10 +2,28 @@ + + + + + + + + + + + + + + + + + + @@ -29,70 +47,14 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -100,38 +62,20 @@ - - - + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - + + @@ -148,36 +92,36 @@ - status - createdAt - preInj - p - printDebug - d - debug - _printDebug - use( - _injec - writeln - write( - _ - index - typed - x - _afterP - _beforeP - optim - ??= dump void li _onError Unhandled exc handleAn Tom Fo - handleRequest( transform( grab( json( + cons + const + call + use( + value + whenI + before + afterProcessed + onco + StreamC + onCo + fatalErrorStream + handleReq + handleRequest( + _Lockabl + takeBy + from + createR + sendRes + replaceAll + noSuch _isClosed @@ -187,9 +131,14 @@ update remove data + event + after + fatalErrorStream + onController C:\Users\thosa\Source\Angel\framework\lib + C:\Users\thosa\Source\Angel\framework\lib\src\http @@ -203,37 +152,43 @@ @@ -267,8 +222,6 @@ - - @@ -291,21 +244,11 @@ - - - - - - @@ -363,15 +306,31 @@ + + - + @@ -379,6 +338,7 @@ + - + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -574,7 +563,8 @@ - + + 1481237183504 @@ -751,43 +741,50 @@ - - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - @@ -802,28 +799,28 @@ - + - - + - + + + + + - - - @@ -857,119 +854,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -998,13 +890,6 @@ - - - - - - - @@ -1033,13 +918,6 @@ - - - - - - - @@ -1047,13 +925,6 @@ - - - - - - - @@ -1061,13 +932,6 @@ - - - - - - - @@ -1089,20 +953,6 @@ - - - - - - - - - - - - - - @@ -1124,21 +974,6 @@ - - - - - - - - - - - - - - - @@ -1146,14 +981,6 @@ - - - - - - - - @@ -1162,27 +989,6 @@ - - - - - - - - - - - - - - - - - - - - - @@ -1191,13 +997,6 @@ - - - - - - - @@ -1206,10 +1005,203 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1217,18 +1209,72 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/CHANGELOG.md b/CHANGELOG.md index d6719ced..db0ebd82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 1.0.8 +* Changed `req.query` to use a modifiable Map if the body has not parsed. Resolves +[#157](https://github.com/angel-dart/framework/issues/157). +* Changed all constants to `camelCase`, and deprecated their `CONSTANT_CASE` counterparts. Resolves +[#155](https://github.com/angel-dart/framework/issues/155). +* Resolved [#156](https://github.com/angel-dart/framework/issues/156) by adding a `graphql` provider. +* Added an `analysis-options.yaml` enabling strong mode. Preparing for Dart 2.0. +* Added a dependency on `package:meta`, resolving [#154](https://github.com/angel-dart/framework/issues/154), +and added corresponding annotations to make extending Angel easier. +* Resolved [#158](https://github.com/angel-dart/framework/issues/158) by using proper `StreamController` +patterns, to prevent memory leaks. +* Route handler sequences are now cached in a Map, so repeat requests will be resolved faster. +* A message is no longer printed in production mode. +* Removed the inheritance on `Extensible` in many classes, and removed it from `angel_route`. +Now, only `Angel` and `RequestContext` have `@proxy` annotations. +* Deprecated passing `debug` to Angel. + # 1.0.7+2 Changed `ResponseContext.serialize`. The `contentType` is now set *before* serialization. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..518eb901 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,2 @@ +analyzer: + strong-mode: true \ No newline at end of file diff --git a/lib/hooks.dart b/lib/hooks.dart index 19eca064..4e165c47 100644 --- a/lib/hooks.dart +++ b/lib/hooks.dart @@ -143,8 +143,6 @@ HookedServiceEventListener remove(key, [remover(key, obj)]) { return obj.where((k) => !key); else if (obj is Map) return obj..remove(key); - else if (obj is Extensible) - return obj..properties.remove(key); else { try { reflect(obj).setField(new Symbol(key), null); @@ -231,8 +229,6 @@ HookedServiceEventListener addCreatedAt( return assign(obj, now); else if (obj is Map) obj[name] = now; - else if (obj is Extensible) - obj..properties[name] = now; else { try { reflect(obj).setField(new Symbol(name), now); @@ -282,8 +278,6 @@ HookedServiceEventListener addUpdatedAt( return assign(obj, now); else if (obj is Map) obj[name] = now; - else if (obj is Extensible) - obj..properties[name] = now; else { try { reflect(obj).setField(new Symbol(name), now); diff --git a/lib/src/extensible.dart b/lib/src/extensible.dart deleted file mode 100644 index 45ad12d4..00000000 --- a/lib/src/extensible.dart +++ /dev/null @@ -1,3 +0,0 @@ -library angel_framework.extensible; - -export 'package:angel_route/src/extensible.dart'; \ No newline at end of file diff --git a/lib/src/fast_name_from_symbol.dart b/lib/src/fast_name_from_symbol.dart new file mode 100644 index 00000000..1460117d --- /dev/null +++ b/lib/src/fast_name_from_symbol.dart @@ -0,0 +1,6 @@ +String fastNameFromSymbol(Symbol s) { + String str = s.toString(); + int open = str.indexOf('"'); + int close = str.lastIndexOf('"'); + return str.substring(open + 1, close); +} \ No newline at end of file diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart index 2c869965..297f1356 100644 --- a/lib/src/http/angel_base.dart +++ b/lib/src/http/angel_base.dart @@ -2,17 +2,19 @@ library angel_framework.http.angel_base; import 'dart:async'; import 'package:container/container.dart'; +import '../fast_name_from_symbol.dart'; import 'routable.dart'; /// A function that asynchronously generates a view from the given path and data. typedef Future ViewGenerator(String path, [Map data]); /// Base class for Angel servers. Do not bother extending this. +@proxy class AngelBase extends Routable { - AngelBase({bool debug: false}):super(debug: debug); - Container _container = new Container(); + final Map properties = {}; + /// When set to true, the request body will not be parsed /// automatically. You can call `req.parse()` manually, /// or use `lazyBody()`. @@ -29,4 +31,22 @@ class AngelBase extends Routable { /// /// Called by [ResponseContext]@`render`. ViewGenerator viewGenerator = (String view, [Map data]) async => "No view engine has been configured yet."; + + operator [](key) => properties[key]; + operator []=(key, value) => properties[key] = value; + + noSuchMethod(Invocation invocation) { + if (invocation.memberName != null) { + String name = fastNameFromSymbol(invocation.memberName); + + if (invocation.isMethod) { + return Function.apply(properties[name], invocation.positionalArguments, + invocation.namedArguments); + } else if (invocation.isGetter) { + return properties[name]; + } + } + + return super.noSuchMethod(invocation); + } } \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 32b5c1bd..beb760dd 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -3,6 +3,7 @@ library angel_framework.http.controller; import 'dart:async'; import 'dart:mirrors'; import 'package:angel_route/angel_route.dart'; +import 'package:meta/meta.dart'; import 'metadata.dart'; import 'request_context.dart'; import 'response_context.dart'; @@ -18,13 +19,20 @@ import 'server.dart' show Angel, preInject; /// and memory use. class InjectionRequest { /// Optional, typed data that can be passed to a DI-enabled method. - Map named = {}; + final Map named; /// A list of the arguments required for a DI-enabled method to run. - final List required = []; + final List required; /// A list of the arguments that can be null in a DI-enabled method. - final List optional = []; + final List optional; + + const InjectionRequest.constant({this.named, this.required, this.optional}); + + InjectionRequest() + : named = {}, + required = [], + optional = []; } /// Supports grouping routes with shared functionality. @@ -47,6 +55,7 @@ class Controller { Controller({this.debug: false, this.injectSingleton: true}); + @mustCallSuper Future call(Angel app) async { _app = app; @@ -143,6 +152,7 @@ RequestHandler createDynamicHandler(handler, injection.optional.addAll(optional ?? []); return handleContained(handler, injection); } + /// Handles a request with a DI-enabled handler. RequestHandler handleContained(handler, InjectionRequest injection) { return (RequestContext req, ResponseContext res) async { diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart index 2e2cc77c..3e3fa0d5 100644 --- a/lib/src/http/hooked_service.dart +++ b/lib/src/http/hooked_service.dart @@ -108,27 +108,27 @@ class HookedService extends Service { applyListeners(inner.index, beforeIndexed); applyListeners(inner.read, beforeRead); - applyListeners(inner.created, beforeCreated); + applyListeners(inner.create, beforeCreated); applyListeners(inner.modify, beforeModified); - applyListeners(inner.updated, beforeUpdated); - applyListeners(inner.removed, beforeRemoved); + applyListeners(inner.update, beforeUpdated); + applyListeners(inner.remove, beforeRemoved); applyListeners(inner.index, afterIndexed, true); applyListeners(inner.read, afterRead, true); - applyListeners(inner.created, afterCreated, true); + applyListeners(inner.create, afterCreated, true); applyListeners(inner.modify, afterModified, true); - applyListeners(inner.updated, afterUpdated, true); - applyListeners(inner.removed, afterRemoved, true); + applyListeners(inner.update, afterUpdated, true); + applyListeners(inner.remove, afterRemoved, true); } /// Adds routes to this instance. @override void addRoutes() { // Set up our routes. We still need to copy middleware from inner service - Map restProvider = {'provider': Providers.REST}; + Map restProvider = {'provider': Providers.rest}; // Add global middleware if declared on the instance itself Middleware before = getAnnotation(inner, Middleware); - final handlers = [ + List handlers = [ (RequestContext req, ResponseContext res) async { req.serviceParams ..['__requestctx'] = req @@ -270,17 +270,17 @@ class HookedService extends Service { Iterable eventNames, HookedServiceEventListener listener) { eventNames.map((name) { switch (name) { - case HookedServiceEvent.INDEXED: + case HookedServiceEvent.indexed: return beforeIndexed; - case HookedServiceEvent.READ: + case HookedServiceEvent.read: return beforeRead; - case HookedServiceEvent.CREATED: + case HookedServiceEvent.created: return beforeCreated; - case HookedServiceEvent.MODIFIED: + case HookedServiceEvent.modified: return beforeModified; - case HookedServiceEvent.UPDATED: + case HookedServiceEvent.updated: return beforeUpdated; - case HookedServiceEvent.REMOVED: + case HookedServiceEvent.removed: return beforeRemoved; default: throw new ArgumentError('Invalid service method: ${name}'); @@ -293,17 +293,17 @@ class HookedService extends Service { void after(Iterable eventNames, HookedServiceEventListener listener) { eventNames.map((name) { switch (name) { - case HookedServiceEvent.INDEXED: + case HookedServiceEvent.indexed: return afterIndexed; - case HookedServiceEvent.READ: + case HookedServiceEvent.read: return afterRead; - case HookedServiceEvent.CREATED: + case HookedServiceEvent.created: return afterCreated; - case HookedServiceEvent.MODIFIED: + case HookedServiceEvent.modified: return afterModified; - case HookedServiceEvent.UPDATED: + case HookedServiceEvent.updated: return afterUpdated; - case HookedServiceEvent.REMOVED: + case HookedServiceEvent.removed: return afterRemoved; default: throw new ArgumentError('Invalid service method: ${name}'); @@ -340,7 +340,7 @@ class HookedService extends Service { Stream beforeAllStream() { var ctrl = new StreamController(); _ctrl.add(ctrl); - before(HookedServiceEvent.ALL, ctrl.add); + before(HookedServiceEvent.all, ctrl.add); return ctrl.stream; } @@ -352,7 +352,7 @@ class HookedService extends Service { Stream afterAllStream() { var ctrl = new StreamController(); _ctrl.add(ctrl); - before(HookedServiceEvent.ALL, ctrl.add); + before(HookedServiceEvent.all, ctrl.add); return ctrl.stream; } @@ -392,12 +392,12 @@ class HookedService extends Service { var params = _stripReq(_params); HookedServiceEvent before = await beforeIndexed._emit( new HookedServiceEvent(false, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.INDEXED, + _getResponse(_params), inner, HookedServiceEvent.indexed, params: params)); if (before._canceled) { HookedServiceEvent after = await beforeIndexed._emit( new HookedServiceEvent(true, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.INDEXED, + _getResponse(_params), inner, HookedServiceEvent.indexed, params: params, result: before.result)); return after.result; } @@ -408,7 +408,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.INDEXED, + HookedServiceEvent.indexed, params: params, result: result)); return after.result; @@ -422,7 +422,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.READ, + HookedServiceEvent.indexed, id: id, params: params)); @@ -432,7 +432,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.READ, + HookedServiceEvent.read, id: id, params: params, result: before.result)); @@ -445,7 +445,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.READ, + HookedServiceEvent.read, id: id, params: params, result: result)); @@ -457,13 +457,13 @@ class HookedService extends Service { var params = _stripReq(_params); HookedServiceEvent before = await beforeCreated._emit( new HookedServiceEvent(false, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.CREATED, + _getResponse(_params), inner, HookedServiceEvent.created, data: data, params: params)); if (before._canceled) { HookedServiceEvent after = await afterCreated._emit( new HookedServiceEvent(true, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.CREATED, + _getResponse(_params), inner, HookedServiceEvent.created, data: data, params: params, result: before.result)); return after.result; } @@ -474,7 +474,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.CREATED, + HookedServiceEvent.created, data: data, params: params, result: result)); @@ -486,13 +486,13 @@ class HookedService extends Service { var params = _stripReq(_params); HookedServiceEvent before = await beforeModified._emit( new HookedServiceEvent(false, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.MODIFIED, + _getResponse(_params), inner, HookedServiceEvent.modified, id: id, data: data, params: params)); if (before._canceled) { HookedServiceEvent after = await afterModified._emit( new HookedServiceEvent(true, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.MODIFIED, + _getResponse(_params), inner, HookedServiceEvent.modified, id: id, data: data, params: params, result: before.result)); return after.result; } @@ -503,7 +503,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.MODIFIED, + HookedServiceEvent.modified, id: id, data: data, params: params, @@ -516,13 +516,13 @@ class HookedService extends Service { var params = _stripReq(_params); HookedServiceEvent before = await beforeUpdated._emit( new HookedServiceEvent(false, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.UPDATED, + _getResponse(_params), inner, HookedServiceEvent.updated, id: id, data: data, params: params)); if (before._canceled) { HookedServiceEvent after = await afterUpdated._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)); return after.result; } @@ -533,7 +533,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.UPDATED, + HookedServiceEvent.updated, id: id, data: data, params: params, @@ -546,13 +546,13 @@ class HookedService extends Service { var params = _stripReq(_params); HookedServiceEvent before = await beforeRemoved._emit( new HookedServiceEvent(false, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.REMOVED, + _getResponse(_params), inner, HookedServiceEvent.removed, id: id, params: params)); if (before._canceled) { HookedServiceEvent after = await afterRemoved._emit( new HookedServiceEvent(true, _getRequest(_params), - _getResponse(_params), inner, HookedServiceEvent.REMOVED, + _getResponse(_params), inner, HookedServiceEvent.removed, id: id, params: params, result: before.result)); return after.result; } @@ -563,7 +563,7 @@ class HookedService extends Service { _getRequest(_params), _getResponse(_params), inner, - HookedServiceEvent.REMOVED, + HookedServiceEvent.removed, id: id, params: params, result: result)); @@ -577,22 +577,22 @@ class HookedService extends Service { HookedServiceEventDispatcher dispatcher; switch (eventName) { - case HookedServiceEvent.INDEXED: + case HookedServiceEvent.indexed: dispatcher = afterIndexed; break; - case HookedServiceEvent.READ: + case HookedServiceEvent.read: dispatcher = afterRead; break; - case HookedServiceEvent.CREATED: + case HookedServiceEvent.created: dispatcher = afterCreated; break; - case HookedServiceEvent.MODIFIED: + case HookedServiceEvent.modified: dispatcher = afterModified; break; - case HookedServiceEvent.UPDATED: + case HookedServiceEvent.updated: dispatcher = afterUpdated; break; - case HookedServiceEvent.REMOVED: + case HookedServiceEvent.removed: dispatcher = afterRemoved; break; default: @@ -614,20 +614,44 @@ class HookedService extends Service { /// Fired when a hooked service is invoked. class HookedServiceEvent { - static const String INDEXED = "indexed"; - static const String READ = "read"; - static const String CREATED = "created"; - static const String MODIFIED = "modified"; - static const String UPDATED = "updated"; - static const String REMOVED = "removed"; - static const List ALL = const [ - INDEXED, - READ, - CREATED, - MODIFIED, - UPDATED, - REMOVED + static const String indexed = 'indexed'; + static const String read = 'read'; + static const String created = 'created'; + static const String modified = 'modified'; + static const String updated = 'updated'; + static const String removed = 'removed'; + + static const List all = const [ + indexed, read, created, modified, updated, removed ]; + + /// Use [indexed] instead. + @deprecated + static const String INDEXED = indexed; + + /// Use [read] instead. + @deprecated + static const String READ = read; + + /// Use [created] instead. + @deprecated + static const String CREATED = created; + + /// Use [modified] instead. + @deprecated + static const String MODIFIED = modified; + + /// Use [updated] instead. + @deprecated + static const String UPDATED = updated; + + /// Use [removed] instead. + @deprecated + static const String REMOVED = removed; + + /// Use [all] instead. + @deprecated + static const List ALL = all; /// Use this to end processing of an event. void cancel([result]) { diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index 73dcbd5e..086ed951 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -2,18 +2,23 @@ library angel_framework.http.request_context; import 'dart:async'; import 'dart:io'; -import 'package:angel_route/src/extensible.dart'; import 'package:body_parser/body_parser.dart'; +import 'package:charcode/charcode.dart'; +import '../fast_name_from_symbol.dart'; import 'server.dart' show Angel; /// A convenience wrapper around an incoming HTTP request. -class RequestContext extends Extensible { +@proxy +class RequestContext { String _acceptHeaderCache; bool _acceptsAllCache; BodyParseResult _body; ContentType _contentType; HttpRequest _io; String _override, _path; + Map _provisionalQuery; + + final Map properties = {}; /// Additional params to be passed to services. final Map serviceParams = {}; @@ -106,7 +111,7 @@ class RequestContext extends Extensible { /// **If you are writing a plug-in, consider using [lazyQuery] instead.** Map get query { if (_body == null) - return uri.queryParameters; + return _provisionalQuery ??= new Map.from(uri.queryParameters); else return _body.query; } @@ -145,16 +150,36 @@ class RequestContext extends Extensible { ctx.app = app; ctx._contentType = request.headers.contentType; ctx._override = override; - ctx._path = request.uri - .toString() - .replaceAll("?" + request.uri.query, "") - .replaceAll(new RegExp(r'/+$'), ''); + + // Faster way to get path + List _path = []; + + // Go up until we reach a ? + for (int ch in request.uri.toString().codeUnits) { + if (ch != $question) + _path.add(ch); + else break; + } + + // Remove trailing slashes + int lastSlash = -1; + + for (int i = _path.length - 1; i >= 0; i--) { + if (_path[i] == $slash) + lastSlash = i; + else break; + } + + if (lastSlash > -1) + ctx._path = new String.fromCharCodes(_path.take(lastSlash)); + else ctx._path = new String.fromCharCodes(_path); ctx._io = request; - if (app.lazyParseBodies != true) + if (app.lazyParseBodies != true) { ctx._body = (await parseBody(request, - storeOriginalBuffer: app.storeOriginalBuffer == true)) ?? + storeOriginalBuffer: app.storeOriginalBuffer == true)) ?? {}; + } return ctx; } @@ -246,7 +271,26 @@ class RequestContext extends Extensible { if (_body != null) return _body; else + _provisionalQuery = null; return _body = await parseBody(io, storeOriginalBuffer: app.storeOriginalBuffer == true); } + + operator [](key) => properties[key]; + operator []=(key, value) => properties[key] = value; + + noSuchMethod(Invocation invocation) { + if (invocation.memberName != null) { + String name = fastNameFromSymbol(invocation.memberName); + + if (invocation.isMethod) { + return Function.apply(properties[name], invocation.positionalArguments, + invocation.namedArguments); + } else if (invocation.isGetter) { + return properties[name]; + } + } + + return super.noSuchMethod(invocation); + } } diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 9ce80e0b..08bd6972 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -3,10 +3,10 @@ library angel_framework.http.response_context; import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:angel_route/angel_route.dart'; import 'package:json_god/json_god.dart' as god; import 'package:mime/mime.dart'; -import '../extensible.dart'; import 'server.dart' show Angel; import 'controller.dart'; @@ -19,9 +19,10 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); typedef String ResponseSerializer(data); /// A convenience wrapper around an outgoing HTTP request. -class ResponseContext extends Extensible implements StringSink { +class ResponseContext implements StringSink { final _LockableBytesBuilder _buffer = new _LockableBytesBuilder(); final Map _headers = {HttpHeaders.SERVER: 'angel'}; + final Map properties = {}; bool _isOpen = true, _isClosed = false; int _statusCode = 200; @@ -366,7 +367,7 @@ abstract class _LockableBytesBuilder extends BytesBuilder { class _LockableBytesBuilderImpl implements _LockableBytesBuilder { bool _closed = false; - final List _data = []; + Uint8List _data = new Uint8List(0); StateError _deny() => new StateError('Cannot modified a closed response\'s buffer.'); @@ -385,8 +386,19 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder { void add(List bytes) { if (_closed) throw _deny(); - else { - _data.addAll(bytes); + else if (bytes.isNotEmpty) { + int len = _data.length + bytes.length; + var d = new Uint8List(len); + + for (int i = 0; i < _data.length; i++) { + d[i] = _data[i]; + } + + for (int i = 0; i < bytes.length; i++) { + d[i + _data.length] = bytes[i]; + } + + _data = d; } } @@ -395,7 +407,15 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder { if (_closed) throw _deny(); else { - _data.add(byte); + int len = _data.length + 1; + var d = new Uint8List(len); + + for (int i = 0; i < _data.length; i++) { + d[i] = _data[i]; + } + + d[_data.length] = byte; + _data = d; } } @@ -404,7 +424,7 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder { if (_closed) throw _deny(); else { - _data.clear(); + for (int i = 0; i < _data.length; i++) _data[i] = 0; } } @@ -422,7 +442,7 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder { if (_closed) return toBytes(); else { - var r = new List.from(_data); + var r = new Uint8List.fromList(_data); clear(); return r; } diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index c01cfd61..6102b503 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -2,6 +2,7 @@ library angel_framework.http.routable; import 'dart:async'; import 'package:angel_route/angel_route.dart'; +import 'package:meta/meta.dart'; import '../util.dart'; import 'angel_base.dart'; import 'controller.dart'; @@ -36,6 +37,7 @@ RequestMiddleware waterfall(List handlers) { class Routable extends Router { final Map _controllers = {}; final Map _services = {}; + final Map properties = {}; Routable({bool debug: false}) : super(debug: debug); @@ -59,7 +61,7 @@ class Routable extends Router { /// Assigns a middleware to a name for convenience. @override - registerMiddleware(String name, RequestMiddleware middleware) => + registerMiddleware(String name, @checked RequestHandler middleware) => super.registerMiddleware(name, middleware); /// Retrieves the service assigned to the given path. @@ -129,17 +131,19 @@ class Routable extends Router { } // Also copy properties... - Map copiedProperties = new Map.from(router.properties); - for (String propertyName in copiedProperties.keys) { - properties.putIfAbsent("$middlewarePrefix$propertyName", - () => copiedMiddleware[propertyName]); + if (router is Routable) { + Map copiedProperties = new Map.from(router.properties); + for (String propertyName in copiedProperties.keys) { + properties.putIfAbsent("$middlewarePrefix$propertyName", + () => copiedMiddleware[propertyName]); + } } // _router.dumpTree(header: 'Mounting on "$path":'); // root.child(path, debug: debug, handlers: handlers).addChild(router.root); mount(path, _router); - if (router is Routable) { + if (_router is Routable) { // Copy services, too. :) for (Pattern servicePath in _router._services.keys) { String newServicePath = @@ -149,6 +153,8 @@ class Routable extends Router { } } - if (service != null) _onService.add(service); + if (service != null) { + if (_onService.hasListener) _onService.add(service); + } } } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 357e3eaa..8a85cd53 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -3,10 +3,13 @@ library angel_framework.http.server; import 'dart:async'; import 'dart:io'; import 'dart:mirrors'; -import 'package:angel_route/angel_route.dart'; +import 'package:angel_route/angel_route.dart' hide Extensible; +import 'package:charcode/charcode.dart'; export 'package:container/container.dart'; import 'package:flatten/flatten.dart'; import 'package:json_god/json_god.dart' as god; +import 'package:meta/meta.dart'; +import '../safe_stream_controller.dart'; import 'angel_base.dart'; import 'angel_http_exception.dart'; import 'controller.dart'; @@ -31,18 +34,18 @@ typedef Future AngelConfigurer(Angel app); /// A powerful real-time/REST/MVC server class. class Angel extends AngelBase { - StreamController _afterProcessed = - new StreamController.broadcast(); - StreamController _beforeProcessed = - new StreamController.broadcast(); - StreamController _fatalErrorStream = - new StreamController.broadcast(); - StreamController _onController = - new StreamController.broadcast(); + SafeCtrl _afterProcessed = new SafeCtrl.broadcast(); + SafeCtrl _beforeProcessed = + new SafeCtrl.broadcast(); + SafeCtrl _fatalErrorStream = + new SafeCtrl.broadcast(); + SafeCtrl _onController = new SafeCtrl.broadcast(); + final List _children = []; Router _flattened; bool _isProduction; Angel _parent; + final Map _handlerCache = {}; ServerGenerator _serverGenerator = HttpServer.bind; /// A global Map of manual injections. You usually will not want to touch this. @@ -152,7 +155,7 @@ class Angel extends AngelBase { } @override - Route addRoute(String method, String path, Object handler, + Route addRoute(String method, Pattern path, Object handler, {List middleware: const []}) { if (_flattened != null) { print( @@ -308,9 +311,9 @@ class Angel extends AngelBase { }); } - Future createResponseContext(HttpResponse response) async => - new ResponseContext(response, this) - ..serializer = (_serializer ?? god.serialize); + Future createResponseContext(HttpResponse response) => + new Future.value(new ResponseContext(response, this) + ..serializer = (_serializer ?? god.serialize)); /// Attempts to find a middleware by the given name within this application. findMiddleware(key) { @@ -361,42 +364,50 @@ class Angel extends AngelBase { try { var req = await createRequestContext(request); var res = await createResponseContext(request.response); - String requestedUrl = request.uri.path.replaceAll(_straySlashes, ''); + String requestedUrl; + + // Faster way to get path + List _path = request.uri.path.codeUnits; + + // Remove trailing slashes + int lastSlash = -1; + + for (int i = _path.length - 1; i >= 0; i--) { + if (_path[i] == $slash) + lastSlash = i; + else + break; + } + + if (lastSlash > -1) + requestedUrl = new String.fromCharCodes(_path.take(lastSlash)); + else + requestedUrl = new String.fromCharCodes(_path); if (requestedUrl.isEmpty) requestedUrl = '/'; - Router r = - isProduction ? (_flattened ?? (_flattened = flatten(this))) : this; - var resolved = - r.resolveAll(requestedUrl, requestedUrl, method: req.method); + var pipeline = _handlerCache.putIfAbsent(requestedUrl, () { + Router r = + isProduction ? (_flattened ?? (_flattened = flatten(this))) : this; + var resolved = + r.resolveAll(requestedUrl, requestedUrl, method: req.method); - for (var result in resolved) req.params.addAll(result.allParams); + for (var result in resolved) req.params.addAll(result.allParams); - if (resolved.isNotEmpty) { - var route = resolved.first.route; - req.inject(Match, route.match(requestedUrl)); - } + if (resolved.isNotEmpty) { + var route = resolved.first.route; + req.inject(Match, route.match(requestedUrl)); + } - var m = new MiddlewarePipeline(resolved); - req.inject(MiddlewarePipeline, m); + var m = new MiddlewarePipeline(resolved); + req.inject(MiddlewarePipeline, m); - var pipeline = []..addAll(before)..addAll(m.handlers)..addAll(after); - - // _printDebug('Handler sequence on $requestedUrl: $pipeline'); + return new List.from(before)..addAll(m.handlers)..addAll(after); + }); for (var handler in pipeline) { try { - // _printDebug('Executing handler: $handler'); - var result = await executeHandler(handler, req, res); - // _printDebug('Result: $result'); - - if (!result) { - // _printDebug('Last executed handler: $handler'); - break; - } else { - // _printDebug( - // 'Handler completed successfully, did not terminate response: $handler'); - } + if (!await executeHandler(handler, req, res)) break; } on AngelHttpException catch (e, st) { e.stackTrace ??= st; return await handleAngelHttpException(e, st, req, res, request); @@ -448,7 +459,8 @@ class Angel extends AngelBase { if (_flattened == null) _flattened = flatten(this); _walk(_flattened); - print('Angel is running in production mode.'); + + //if (silent != true) print('Angel is running in production mode.'); } } @@ -482,15 +494,16 @@ class Angel extends AngelBase { /// Sends a response. Future sendResponse( HttpRequest request, RequestContext req, ResponseContext res, - {bool ignoreFinalizers: false}) async { + {bool ignoreFinalizers: false}) { _afterProcessed.add(request); - if (!res.willCloseItself) { - if (ignoreFinalizers != true) { - for (var finalizer in responseFinalizers) { - await finalizer(req, res); - } - } + if (res.willCloseItself) { + return new Future.value(); + } else { + Future finalizers = ignoreFinalizers == true + ? new Future.value() + : responseFinalizers.fold( + new Future.value(), (out, f) => out.then((_) => f(req, res))); if (res.isOpen) res.end(); @@ -505,8 +518,9 @@ class Angel extends AngelBase { request.response ..statusCode = res.statusCode ..cookies.addAll(res.cookies) - ..add(res.buffer.takeBytes()); - await request.response.close(); + ..add(res.buffer.toBytes()); + + return finalizers.then((_) => request.response.close()); } } @@ -541,7 +555,7 @@ class Angel extends AngelBase { /// NOTE: The above will not be properly copied if [path] is /// a [RegExp]. @override - use(Pattern path, Routable routable, + use(Pattern path, @checked Routable routable, {bool hooked: true, String namespace: null}) { var head = path.toString().replaceAll(_straySlashes, ''); @@ -581,6 +595,22 @@ class Angel extends AngelBase { var tail = k.toString().replaceAll(_straySlashes, ''); services['$head/$tail'.replaceAll(_straySlashes, '')] = v; }); + + _beforeProcessed.whenInitialized(() { + routable.beforeProcessed.listen(_beforeProcessed.add); + }); + + _afterProcessed.whenInitialized(() { + routable.afterProcessed.listen(_afterProcessed.add); + }); + + _fatalErrorStream.whenInitialized(() { + routable.fatalErrorStream.listen(_fatalErrorStream.add); + }); + + _onController.whenInitialized(() { + routable.onController.listen(_onController.add); + }); } if (routable is Service) { @@ -597,17 +627,18 @@ class Angel extends AngelBase { } /// Default constructor. ;) - Angel({bool debug: false}) : super(debug: debug == true) { + Angel({@deprecated bool debug: false}) : super() { bootstrapContainer(); } /// An instance mounted on a server started by the [serverGenerator]. - factory Angel.custom(ServerGenerator serverGenerator, {bool debug: false}) => - new Angel(debug: debug == true).._serverGenerator = serverGenerator; + factory Angel.custom(ServerGenerator serverGenerator, + {@deprecated bool debug: false}) => + new Angel().._serverGenerator = serverGenerator; factory Angel.fromSecurityContext(SecurityContext context, - {bool debug: false}) { - var app = new Angel(debug: debug == true); + {@deprecated bool debug: false}) { + var app = new Angel(); app._serverGenerator = (InternetAddress address, int port) async { return await HttpServer.bindSecure(address, port, context); diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 9a27d3f4..ab58c037 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -20,14 +20,34 @@ class Providers { const Providers(String this.via); + static const String viaRest = "rest"; + static const String viaWebsocket = "websocket"; + static const String viaGraphQL = "graphql"; + + /// Use [viaRest] instead. + @deprecated static const String VIA_REST = "rest"; + + /// Use [viaWebSocket] instead. + @deprecated static const String VIA_WEBSOCKET = "websocket"; + /// Use [rest] instead. + @deprecated + static const Providers REST = const Providers(viaRest); + + /// Use [websocket] instead. + @deprecated + static const Providers WEBSOCKET = const Providers(viaWebsocket); + /// Represents a request via REST. - static final Providers REST = const Providers(VIA_REST); + static const Providers rest = const Providers(viaRest); /// Represents a request over WebSockets. - static final Providers WEBSOCKET = const Providers(VIA_WEBSOCKET); + static const Providers websocket = const Providers(viaWebsocket); + + /// Represents a request parsed from GraphQL. + static const Providers graphql = const Providers(viaGraphQL); @override bool operator ==(other) => other is Providers && other.via == via; @@ -38,13 +58,17 @@ class Providers { /// Heavily inspired by FeathersJS. <3 class Service extends Routable { /// A [List] of keys that services should ignore, should they see them in the query. - static const List SPECIAL_QUERY_KEYS = const [ + static const List specialQueryKeys = const [ r'$limit', r'$sort', 'page', 'token' ]; + /// Use [specialQueryKeys] instead. + @deprecated + static const List SPECIAL_QUERY_KEYS = specialQueryKeys; + /// The [Angel] app powering this service. AngelBase app; @@ -91,7 +115,7 @@ class Service extends Routable { /// Generates RESTful routes pointing to this class's methods. void addRoutes() { - Map restProvider = {'provider': Providers.REST}; + Map restProvider = {'provider': Providers.rest}; // Add global middleware if declared on the instance itself Middleware before = getAnnotation(this, Middleware); @@ -107,8 +131,9 @@ class Service extends Routable { req.serviceParams ])); }, - middleware: []..addAll(handlers)..addAll( - (indexMiddleware == null) ? [] : indexMiddleware.handlers)); + middleware: [] + ..addAll(handlers) + ..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers)); Middleware createMiddleware = getAnnotation(this.create, Middleware); post('/', (req, ResponseContext res) async { @@ -122,29 +147,30 @@ class Service extends Routable { res.statusCode = 201; return r; }, - middleware: []..addAll(handlers)..addAll( - (createMiddleware == null) ? [] : createMiddleware.handlers)); + middleware: [] + ..addAll(handlers) + ..addAll( + (createMiddleware == null) ? [] : createMiddleware.handlers)); Middleware readMiddleware = getAnnotation(this.read, Middleware); get( '/:id', - (req, res) async => - await this.read( + (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: [] + ..addAll(handlers) + ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); Middleware modifyMiddleware = getAnnotation(this.modify, Middleware); patch( '/:id', - (req, res) async => - await this.modify( + (req, res) async => await this.modify( toId(req.params['id']), await req.lazyBody(), mergeMap([ @@ -152,14 +178,15 @@ class Service extends Routable { restProvider, req.serviceParams ])), - middleware: []..addAll(handlers)..addAll( - (modifyMiddleware == null) ? [] : modifyMiddleware.handlers)); + middleware: [] + ..addAll(handlers) + ..addAll( + (modifyMiddleware == null) ? [] : modifyMiddleware.handlers)); Middleware updateMiddleware = getAnnotation(this.update, Middleware); post( '/:id', - (req, res) async => - await this.update( + (req, res) async => await this.update( toId(req.params['id']), await req.lazyBody(), mergeMap([ @@ -167,12 +194,13 @@ class Service extends Routable { restProvider, req.serviceParams ])), - middleware: []..addAll(handlers)..addAll( - (updateMiddleware == null) ? [] : updateMiddleware.handlers)); + middleware: [] + ..addAll(handlers) + ..addAll( + (updateMiddleware == null) ? [] : updateMiddleware.handlers)); put( '/:id', - (req, res) async => - await this.update( + (req, res) async => await this.update( toId(req.params['id']), await req.lazyBody(), mergeMap([ @@ -180,34 +208,38 @@ class Service extends Routable { restProvider, req.serviceParams ])), - middleware: []..addAll(handlers)..addAll( - (updateMiddleware == null) ? [] : updateMiddleware.handlers)); + middleware: [] + ..addAll(handlers) + ..addAll( + (updateMiddleware == null) ? [] : updateMiddleware.handlers)); Middleware removeMiddleware = getAnnotation(this.remove, Middleware); delete( '/', - (req, res) async => - await this.remove( + (req, res) async => await this.remove( null, mergeMap([ {'query': req.query}, restProvider, req.serviceParams ])), - middleware: []..addAll(handlers)..addAll( - (removeMiddleware == null) ? [] : removeMiddleware.handlers)); + middleware: [] + ..addAll(handlers) + ..addAll( + (removeMiddleware == null) ? [] : removeMiddleware.handlers)); delete( '/:id', - (req, res) async => - await this.remove( + (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)); + middleware: [] + ..addAll(handlers) + ..addAll( + (removeMiddleware == null) ? [] : removeMiddleware.handlers)); // REST compliance put('/', () => throw new AngelHttpException.notFound()); diff --git a/lib/src/safe_stream_controller.dart b/lib/src/safe_stream_controller.dart new file mode 100644 index 00000000..558d1082 --- /dev/null +++ b/lib/src/safe_stream_controller.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +typedef void _InitCallback(); + +/// A [StreamController] boilerplate that prevents memory leaks. +abstract class SafeCtrl { + factory SafeCtrl() => new _SingleSafeCtrl(); + + factory SafeCtrl.broadcast() => new _BroadcastSafeCtrl(); + + Stream get stream; + + void add(T event); + + void addError(error, [StackTrace stackTrace]); + + Future close(); + + void whenInitialized(void callback()); +} + +class _SingleSafeCtrl implements SafeCtrl { + StreamController _stream; + bool _hasListener = false, _initialized = false; + _InitCallback _initializer; + + _SingleSafeCtrl() { + _stream = new StreamController(onListen: () { + _hasListener = true; + + if (!_initialized && _initializer != null) { + _initializer(); + _initialized = true; + } + }, onPause: () { + _hasListener = false; + }, onResume: () { + _hasListener = true; + }, onCancel: () { + _hasListener = false; + }); + } + + @override + Stream get stream => _stream.stream; + + @override + void add(T event) { + if (_hasListener) _stream.add(event); + } + + @override + void addError(error, [StackTrace stackTrace]) { + if (_hasListener) _stream.addError(error, stackTrace); + } + + @override + Future close() { + return _stream.close(); + } + + @override + void whenInitialized(void callback()) { + if (!_initialized) { + if (!_hasListener) + _initializer = callback; + else { + _initializer(); + _initialized = true; + } + } + } +} + +class _BroadcastSafeCtrl implements SafeCtrl { + StreamController _stream; + int _listeners = 0; + bool _initialized = false; + _InitCallback _initializer; + + _BroadcastSafeCtrl() { + _stream = new StreamController.broadcast(onListen: () { + _listeners++; + + if (!_initialized && _initializer != null) { + _initializer(); + _initialized = true; + } + }, onCancel: () { + _listeners--; + }); + } + + @override + Stream get stream => _stream.stream; + + @override + void add(T event) { + if (_listeners > 0) _stream.add(event); + } + + @override + void addError(error, [StackTrace stackTrace]) { + if (_listeners > 0) _stream.addError(error, stackTrace); + } + + @override + Future close() { + return _stream.close(); + } + + @override + void whenInitialized(void callback()) { + if (!_initialized) { + if (_listeners <= 0) + _initializer = callback; + else { + _initializer(); + _initialized = true; + } + } + } +} diff --git a/performance/hello/main.dart b/performance/hello/main.dart new file mode 100644 index 00000000..405c6cde --- /dev/null +++ b/performance/hello/main.dart @@ -0,0 +1,27 @@ +/// A basic server that prints "Hello, world!" +library performance.hello; + +import 'dart:io'; +import 'dart:isolate'; +import 'package:angel_framework/angel_framework.dart'; + +main() { + for (int i = 0; i < Platform.numberOfProcessors - 1; i++) + Isolate.spawn(start, i + 1); + start(0); +} + +void start(int id) { + var app = new Angel.custom(startShared) + ..lazyParseBodies = true + ..get('/', (req, res) => res.write('Hello, world!')) + ..optimizeForProduction(force: true) + ..fatalErrorStream.listen((e) { + print('Oops: ${e.error}'); + print(e.stack); + }); + app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000).then((server) { + print( + 'Instance #$id listening at http://${server.address.address}:${server.port}'); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 267e1702..344c0dd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,14 +7,15 @@ environment: sdk: ">=1.19.0" dependencies: angel_model: ^1.0.0 - angel_route: ^1.0.0-dev + angel_route: ">=1.0.5 <2.0.0" body_parser: ^1.0.0-dev container: ^0.1.2 flatten: ^1.0.0 json_god: ^2.0.0-beta merge_map: ^1.0.0 - random_string: ^0.0.1 + meta: ^1.0.0 mime: ^0.9.3 + random_string: ^0.0.1 dev_dependencies: mock_request: ^1.0.0 http: ^0.11.3 diff --git a/test/exception_test.dart b/test/exception_test.dart index f1db47ff..ae00dc5d 100644 --- a/test/exception_test.dart +++ b/test/exception_test.dart @@ -2,58 +2,35 @@ import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:matcher/matcher.dart'; +import 'package:meta/meta.dart'; import 'package:test/test.dart'; main() { test('named constructors', () { expect(new AngelHttpException.badRequest(), isException(HttpStatus.BAD_REQUEST, '400 Bad Request')); - expect(new AngelHttpException.BadRequest(), - isException(HttpStatus.BAD_REQUEST, '400 Bad Request')); expect(new AngelHttpException.notAuthenticated(), isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated')); - expect(new AngelHttpException.NotAuthenticated(), - isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated')); expect(new AngelHttpException.paymentRequired(), isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required')); - expect(new AngelHttpException.PaymentRequired(), - isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required')); expect(new AngelHttpException.forbidden(), isException(HttpStatus.FORBIDDEN, '403 Forbidden')); - expect(new AngelHttpException.Forbidden(), - isException(HttpStatus.FORBIDDEN, '403 Forbidden')); expect(new AngelHttpException.notFound(), isException(HttpStatus.NOT_FOUND, '404 Not Found')); - expect(new AngelHttpException.NotFound(), - isException(HttpStatus.NOT_FOUND, '404 Not Found')); expect(new AngelHttpException.methodNotAllowed(), isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed')); - expect(new AngelHttpException.MethodNotAllowed(), - isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed')); expect(new AngelHttpException.notAcceptable(), isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable')); - expect(new AngelHttpException.NotAcceptable(), - isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable')); expect(new AngelHttpException.methodTimeout(), isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout')); - expect(new AngelHttpException.MethodTimeout(), - isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout')); expect(new AngelHttpException.conflict(), isException(HttpStatus.CONFLICT, '409 Conflict')); - expect(new AngelHttpException.Conflict(), - isException(HttpStatus.CONFLICT, '409 Conflict')); expect(new AngelHttpException.notProcessable(), isException(422, '422 Not Processable')); - expect(new AngelHttpException.NotProcessable(), - isException(422, '422 Not Processable')); expect(new AngelHttpException.notImplemented(), isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented')); - expect(new AngelHttpException.NotImplemented(), - isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented')); expect(new AngelHttpException.unavailable(), isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable')); - expect(new AngelHttpException.Unavailable(), - isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable')); }); test('fromMap', () { @@ -91,7 +68,7 @@ class _IsException extends Matcher { description.add('has status code $statusCode and message "$message"'); @override - bool matches(AngelHttpException item, Map matchState) { + bool matches(@checked AngelHttpException item, Map matchState) { return item.statusCode == statusCode && item.message == message; } } diff --git a/test/routing_test.dart b/test/routing_test.dart index 183ac2f6..e6c955de 100644 --- a/test/routing_test.dart +++ b/test/routing_test.dart @@ -13,9 +13,7 @@ testMiddlewareMetadata(RequestContext req, ResponseContext res) async { class QueryService extends Service { @override @Middleware(const ['interceptor']) - read(id, [Map params]) { - return params; - } + read(id, [Map params]) async => params; } main() { diff --git a/test/services_test.dart b/test/services_test.dart index fa402f0a..bfdc4595 100644 --- a/test/services_test.dart +++ b/test/services_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:angel_framework/src/defs.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; @@ -44,13 +45,8 @@ main() { var response = await client.get("$url/todos/"); print(response.body); expect(response.body, equals('[]')); - for (int i = 0; i < 3; i++) { - String postData = god.serialize({'text': 'Hello, world!'}); - await client.post("$url/todos", headers: headers, body: postData); - } - response = await client.get("$url/todos"); print(response.body); - expect(god.deserialize(response.body).length, equals(3)); + expect(JSON.decode(response.body).length, 0); }); test('can create data', () async { @@ -100,15 +96,12 @@ main() { test('can delete data', () async { String postData = god.serialize({'text': 'Hello, world!'}); - await client.post("$url/todos", headers: headers, body: postData); - var response = await client.delete("$url/todos/0"); + var created = await client.post("$url/todos", headers: headers, body: postData).then((r) => JSON.decode(r.body)); + var response = await client.delete("$url/todos/${created['id']}"); expect(response.statusCode, 200); var json = god.deserialize(response.body); print(json); expect(json['text'], equals('Hello, world!')); - response = await client.get("$url/todos"); - print(response.body); - expect(god.deserialize(response.body).length, equals(0)); }); }); }