diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 56a70f18..2cf90842 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,13 +1,19 @@ - - - - - + + + + + + + + + + + @@ -31,7 +37,7 @@ - + @@ -41,61 +47,42 @@ - - + + - - - - - + + + - - + + - - - - - + + + - - + + - - - - - + + + - + - - - - - - - - - - - - - - - + + @@ -106,7 +93,7 @@ - + @@ -115,15 +102,44 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -139,28 +155,6 @@ - dump - void li - _onError - Unhandled exc - handleAn - Tom Fo - transform( - grab( - json( - cons - const - call - use( - value - whenI - before - afterProcessed - onco - StreamC - onCo - fatalErrorStream - handleReq handleRequest( _Lockabl takeBy @@ -169,6 +163,28 @@ sendRes replaceAll noSuch + addStre + finalizers + write + deprec + addStr + addSt + cancell + corr + buffer. + if (_isClosed) + if (_isClosed + createRespo + injectSerializer + encoders + addAll + res.enco + encodingNa + addErr + close() + .complete( + isCompl + streamFil` _isClosed @@ -182,6 +198,7 @@ after fatalErrorStream onController + if (_isClosed && !_useStream) C:\Users\thosa\Source\Angel\framework\lib @@ -209,36 +226,39 @@ @@ -284,6 +304,20 @@ - + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -615,7 +647,8 @@ - + + 1481237183504 @@ -813,43 +846,50 @@ - - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - @@ -869,15 +909,14 @@ - + - - + - + @@ -886,6 +925,7 @@ + @@ -896,7 +936,6 @@ - @@ -921,55 +960,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1012,13 +1010,6 @@ - - - - - - - @@ -1030,7 +1021,6 @@ - @@ -1038,7 +1028,6 @@ - @@ -1046,7 +1035,6 @@ - @@ -1054,23 +1042,6 @@ - - - - - - - - - - - - - - - - - @@ -1094,9 +1065,6 @@ - - - @@ -1104,9 +1072,6 @@ - - - @@ -1114,7 +1079,6 @@ - @@ -1122,7 +1086,6 @@ - @@ -1130,9 +1093,6 @@ - - - @@ -1140,9 +1100,6 @@ - - - @@ -1150,7 +1107,6 @@ - @@ -1158,7 +1114,6 @@ - @@ -1167,30 +1122,11 @@ - - - - - - - - - - - - - - - - - - - @@ -1203,34 +1139,16 @@ - - - - - - - - - + - - - - - - - - - - - + @@ -1239,7 +1157,6 @@ - @@ -1247,7 +1164,6 @@ - @@ -1255,7 +1171,6 @@ - @@ -1263,7 +1178,6 @@ - @@ -1271,63 +1185,19 @@ - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1350,6 +1220,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1358,5 +1314,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fec777b..b44c2ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 1.0.9 +* Closed [#161](https://github.com/angel-dart/framework/issues/161). `addCreated`/`addUpdatedAt` no longer +crash when `serialize` is `false`. +* Added an explicit on `charcode`, `path`, and others. Resolves +[#160](https://github.com/angel-dart/framework/issues/160). +* `ResponseContext` now implements `StreamSink`, so data can be streamed directly to the +underlying response. +* You can now inject `encoders` into a `ResponseContext`, which takes care of `Accept-Encoding`. +This will ultimately replace `package:angel_compress`. +Resolves [#159](https://github.com/angel-dart/framework/issues/159). + # 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). diff --git a/lib/hooks.dart b/lib/hooks.dart index 4e165c47..04975215 100644 --- a/lib/hooks.dart +++ b/lib/hooks.dart @@ -220,11 +220,11 @@ HookedServiceEventListener disable([provider]) { /// /// Default key: `createdAt` HookedServiceEventListener addCreatedAt( - {assign(obj, String now), String key, bool serialize: true}) { + {assign(obj, now), String key, bool serialize: true}) { var name = key?.isNotEmpty == true ? key : 'createdAt'; return (HookedServiceEvent e) async { - _assign(obj, String now) { + _assign(obj, now) { if (assign != null) return assign(obj, now); else if (obj is Map) @@ -269,11 +269,11 @@ HookedServiceEventListener addUpatedAt({ /// /// Default key: `updatedAt` HookedServiceEventListener addUpdatedAt( - {assign(obj, String now), String key, bool serialize: true}) { + {assign(obj, now), String key, bool serialize: true}) { var name = key?.isNotEmpty == true ? key : 'updatedAt'; return (HookedServiceEvent e) async { - _assign(obj, String now) { + _assign(obj, now) { if (assign != null) return assign(obj, now); else if (obj is Map) diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 3ce2b4f7..034f1d5c 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -9,6 +9,7 @@ import 'package:json_god/json_god.dart' as god; import 'package:mime/mime.dart'; import 'server.dart' show Angel; import 'controller.dart'; +import 'request_context.dart'; final RegExp _contentType = new RegExp(r'([^/\n]+)\/\s*([^;\n]+)\s*(;\s*charset=([^$;\n]+))?'); @@ -19,11 +20,14 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); typedef String ResponseSerializer(data); /// A convenience wrapper around an outgoing HTTP request. -class ResponseContext implements StringSink { - final _LockableBytesBuilder _buffer = new _LockableBytesBuilder(); - final Map _headers = {HttpHeaders.SERVER: 'angel'}; +class ResponseContext implements StreamSink>, StringSink { final Map properties = {}; - bool _isOpen = true, _isClosed = false; + final BytesBuilder _buffer = new _LockableBytesBuilder(); + final Map _headers = {HttpHeaders.SERVER: 'angel'}; + final RequestContext _correspondingRequest; + + Completer _done; + bool _isOpen = true, _isClosed = false, _useStream = false; int _statusCode = 200; /// The [Angel] instance that is sending a response. @@ -35,6 +39,17 @@ class ResponseContext implements StringSink { /// Any and all cookies to be sent to the user. final List cookies = []; + /// A set of [Converter] objects that can be used to encode response data. + /// + /// At most one encoder will ever be used to convert data. + final Map, List>> encoders = {}; + + /// Points to the [RequestContext] corresponding to this response. + RequestContext get correspondingRequest => _correspondingRequest; + + @override + Future get done => (_done ?? new Completer()).future; + /// Headers that will be sent to the user. Map get headers { /// If the response is closed, then this getter will return an immutable `Map`. @@ -74,8 +89,8 @@ class ResponseContext implements StringSink { /// A set of UTF-8 encoded bytes that will be written to the response. BytesBuilder get buffer => _buffer; - /// Sets the status code to be sent with this response. - @Deprecated('Please use `statusCode=` instead.') + /// Please use `statusCode=` instead.' + @deprecated void status(int code) { statusCode = code; } @@ -110,7 +125,7 @@ class ResponseContext implements StringSink { headers[HttpHeaders.CONTENT_TYPE] = contentType.toString(); } - ResponseContext(this.io, this.app); + ResponseContext(this.io, this.app, [this._correspondingRequest]); /// Set this to true if you will manually close the response. /// @@ -127,15 +142,32 @@ class ResponseContext implements StringSink { 'attachment; filename="${filename ?? file.path}"'; headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); headers[HttpHeaders.CONTENT_LENGTH] = file.lengthSync().toString(); - buffer.add(await file.readAsBytes()); - end(); + + if (_useStream) { + await file.openRead().pipe(this); + } else { + buffer.add(await file.readAsBytes()); + end(); + } } /// Prevents more data from being written to the response, and locks it entire from further editing. - void close() { - _buffer._lock(); + Future close() { + var f = new Future.value(); + + if (_useStream) { + _useStream = false; + _buffer?.clear(); + f = io.close(); + } else if (_buffer is _LockableBytesBuilder) { + (_buffer as _LockableBytesBuilder)._lock(); + } + _isOpen = false; _isClosed = true; + + if (_done?.isCompleted == false) _done.complete(); + return f; } /// Prevents further request handlers from running on the response, except for response finalizers. @@ -271,7 +303,9 @@ class ResponseContext implements StringSink { /// Copies a file's contents into the response buffer. Future sendFile(File file, - {int chunkSize, int sleepMs: 0, bool resumable: true}) async { + {@deprecated int chunkSize, + @deprecated int sleepMs: 0, + @deprecated bool resumable: true}) async { if (_isClosed) throw _closed(); headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); @@ -297,29 +331,120 @@ class ResponseContext implements StringSink { /// Streams a file to this response. /// + // ignore: deprecated_member_use /// You can optionally transform the file stream with a [codec]. Future streamFile(File file, - {int chunkSize, - int sleepMs: 0, - bool resumable: true, - Codec, List> codec}) async { + {@deprecated int chunkSize, + @deprecated int sleepMs: 0, + @deprecated bool resumable: true, + + /// Use [encoders] instead of manually specifying a codec. + @deprecated Codec, List> codec}) async { if (_isClosed) throw _closed(); headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); - end(); - willCloseItself = true; - Stream stream = codec != null - ? file.openRead().transform(codec.encoder) - : file.openRead(); - await stream.pipe(io); + // ignore: deprecated_member_use + if (codec != null) { + end(); + willCloseItself = true; + + // ignore: deprecated_member_use + Stream stream = codec != null + // ignore: deprecated_member_use + ? file.openRead().transform(codec.encoder) + : file.openRead(); + await stream.pipe(io); + } else { + await file.openRead().pipe(this); + } + } + + @override + void add(List data) { + if (_isClosed && !_useStream) + throw _closed(); + else if (_useStream) + io.add(data); + else + buffer.add(data); + } + + /// Adds a stream directly the underlying dart:[io] response. + /// + /// This will also set [willCloseItself] to `true`, thus canceling out response finalizers. + /// + /// If this instance has access to a [correspondingRequest], then it will attempt to transform + /// the content using at most one of the response [encoders]. + @override + Future addStream(Stream> stream) { + if (_isClosed && !_useStream) throw _closed(); + bool firstStream = _useStream == false; + willCloseItself = _useStream = _isClosed = true; + + Stream> output = stream; + + if (firstStream) { + // If this is the first stream added to this response, + // then add headers, status code, etc. + io + ..statusCode = statusCode + ..cookies.addAll(cookies); + headers.forEach(io.headers.set); + } + + if (encoders.isNotEmpty && correspondingRequest != null) { + var allowedEncodings = + (correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? []) + .map((str) { + // Ignore quality specifications in accept-encoding + // ex. gzip;q=0.8 + if (!str.contains(';')) return str; + return str.split(';')[0]; + }); + + for (var encodingName in allowedEncodings) { + Converter, List> encoder; + String key = encodingName; + + if (encoders.containsKey(encodingName)) + encoder = encoders[encodingName]; + else if (encodingName == '*') { + encoder = encoders[key = encoders.keys.first]; + } + + if (encoder != null) { + if (firstStream) { + io.headers.set(HttpHeaders.CONTENT_ENCODING, key); + } + + output = encoders[key].bind(output); + break; + } + } + } + + return io.addStream(output); + } + + @override + void addError(Object error, [StackTrace stackTrace]) { + io.addError(error, stackTrace); + if (_done?.isCompleted == false) _done.completeError(error, stackTrace); } /// Writes data to the response. - void write(value, {Encoding encoding: UTF8}) { - if (_isClosed) + void write(value, {Encoding encoding}) { + encoding ??= UTF8; + + if (_isClosed && !_useStream) throw _closed(); - else { + else if (_useStream) { + if (value is List) + io.add(value); + else + io.add(encoding.encode(value.toString())); + } else { if (value is List) buffer.add(value); else @@ -329,8 +454,10 @@ class ResponseContext implements StringSink { @override void writeCharCode(int charCode) { - if (_isClosed) + if (_isClosed && !_useStream) throw _closed(); + else if (_useStream) + io.add([charCode]); else buffer.addByte(charCode); } @@ -364,6 +491,7 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder { void _lock() { _closed = true; } + @override void add(List bytes) { if (_closed) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 8a85cd53..0f6dc170 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -1,6 +1,7 @@ library angel_framework.http.server; import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:mirrors'; import 'package:angel_route/angel_route.dart' hide Extensible; @@ -14,6 +15,7 @@ import 'angel_base.dart'; import 'angel_http_exception.dart'; import 'controller.dart'; import 'fatal_error.dart'; + //import 'hooked_service.dart'; import 'request_context.dart'; import 'response_context.dart'; @@ -26,30 +28,37 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); typedef Future ServerGenerator(InternetAddress address, int port); /// Handles an [AngelHttpException]. -typedef Future AngelErrorHandler( - AngelHttpException err, RequestContext req, ResponseContext res); +typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, + ResponseContext res); /// A function that configures an [Angel] server in some way. typedef Future AngelConfigurer(Angel app); /// A powerful real-time/REST/MVC server class. class Angel extends AngelBase { - SafeCtrl _afterProcessed = new SafeCtrl.broadcast(); - SafeCtrl _beforeProcessed = - new SafeCtrl.broadcast(); - SafeCtrl _fatalErrorStream = - new SafeCtrl.broadcast(); - SafeCtrl _onController = new SafeCtrl.broadcast(); + final SafeCtrl _afterProcessed = + new SafeCtrl.broadcast(); + final SafeCtrl _beforeProcessed = + new SafeCtrl.broadcast(); + final SafeCtrl _fatalErrorStream = + new SafeCtrl.broadcast(); + final SafeCtrl _onController = + new SafeCtrl.broadcast(); final List _children = []; + final Map _handlerCache = {}; + Router _flattened; bool _isProduction; Angel _parent; - final Map _handlerCache = {}; ServerGenerator _serverGenerator = HttpServer.bind; + /// A global Map of converters that can transform responses bodies. + final Map, List>> encoders = {}; + /// A global Map of manual injections. You usually will not want to touch this. final Map injections = {}; + final Map _preContained = {}; ResponseSerializer _serializer; @@ -204,17 +213,17 @@ class Angel extends AngelBase { await service.close(); }); - for (var plugin in justBeforeStop) await plugin(this); + for (var plugin in justBeforeStop) + await plugin(this); return server; } @override - void dumpTree( - {callback(String tree), - String header: 'Dumping route tree:', - String tab: ' ', - bool showMatchers: false}) { + void dumpTree({callback(String tree), + String header: 'Dumping route tree:', + String tab: ' ', + bool showMatchers: false}) { if (isProduction) { if (_flattened == null) _flattened = flatten(this); @@ -223,8 +232,8 @@ class Angel extends AngelBase { header: header?.isNotEmpty == true ? header : (isProduction - ? 'Dumping flattened route tree:' - : 'Dumping route tree:'), + ? 'Dumping flattened route tree:' + : 'Dumping route tree:'), tab: tab ?? ' ', showMatchers: showMatchers == true); } else { @@ -233,8 +242,8 @@ class Angel extends AngelBase { header: header?.isNotEmpty == true ? header : (isProduction - ? 'Dumping flattened route tree:' - : 'Dumping route tree:'), + ? 'Dumping flattened route tree:' + : 'Dumping route tree:'), tab: tab ?? ' ', showMatchers: showMatchers == true); } @@ -245,13 +254,18 @@ class Angel extends AngelBase { injections[key] = value; } + /// Shortcuts for adding converters to transform the response buffer/stream of any request. + void injectEncoders(Map, List>> encoders) { + this.encoders.addAll(encoders); + } + /// Shortcut for adding a middleware to inject a serialize on every request. void injectSerializer(ResponseSerializer serializer) { _serializer = serializer; } - Future getHandlerResult( - handler, RequestContext req, ResponseContext res) async { + Future getHandlerResult(handler, RequestContext req, + ResponseContext res) async { /*if (handler is RequestMiddleware) { var result = await handler(req, res); @@ -289,8 +303,8 @@ class Angel extends AngelBase { } /// Runs some [handler]. Returns `true` if request execution should continue. - Future executeHandler( - handler, RequestContext req, ResponseContext res) async { + Future executeHandler(handler, RequestContext req, + ResponseContext res) async { var result = await getHandlerResult(handler, req, res); if (result is bool) { @@ -311,9 +325,12 @@ class Angel extends AngelBase { }); } - Future createResponseContext(HttpResponse response) => - new Future.value(new ResponseContext(response, this) - ..serializer = (_serializer ?? god.serialize)); + Future createResponseContext(HttpResponse response, + [RequestContext correspondingRequest]) => + new Future.value( + new ResponseContext(response, this, correspondingRequest) + ..serializer = (_serializer ?? god.serialize) + ..encoders.addAll(encoders ?? {})); /// Attempts to find a middleware by the given name within this application. findMiddleware(key) { @@ -363,7 +380,7 @@ class Angel extends AngelBase { Future handleRequest(HttpRequest request) async { try { var req = await createRequestContext(request); - var res = await createResponseContext(request.response); + var res = await createResponseContext(request.response, req); String requestedUrl; // Faster way to get path @@ -388,11 +405,12 @@ class Angel extends AngelBase { var pipeline = _handlerCache.putIfAbsent(requestedUrl, () { Router r = - isProduction ? (_flattened ?? (_flattened = flatten(this))) : this; + isProduction ? (_flattened ?? (_flattened = flatten(this))) : this; var resolved = - r.resolveAll(requestedUrl, requestedUrl, method: req.method); + 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; @@ -402,7 +420,8 @@ class Angel extends AngelBase { var m = new MiddlewarePipeline(resolved); req.inject(MiddlewarePipeline, m); - return new List.from(before)..addAll(m.handlers)..addAll(after); + return new List.from(before) + ..addAll(m.handlers)..addAll(after); }); for (var handler in pipeline) { @@ -445,7 +464,9 @@ class Angel extends AngelBase { void _walk(Router router) { if (router is Angel) { - router..before.forEach(_add)..after.forEach(_add); + router + ..before.forEach(_add) + ..after.forEach(_add); } router.requestMiddleware.forEach((k, v) => _add(v)); @@ -467,8 +488,8 @@ class Angel extends AngelBase { /// Run a function after injecting from service container. /// If this function has been reflected before, then /// the execution will be faster, as the injection requirements were stored beforehand. - Future runContained( - Function handler, RequestContext req, ResponseContext res) { + Future runContained(Function handler, RequestContext req, + ResponseContext res) { if (_preContained.containsKey(handler)) { return handleContained(handler, _preContained[handler])(req, res); } @@ -477,23 +498,23 @@ class Angel extends AngelBase { } /// Runs with DI, and *always* reflects. Prefer [runContained]. - Future runReflected( - Function handler, RequestContext req, ResponseContext res) async { + Future runReflected(Function handler, RequestContext req, + ResponseContext res) async { var h = - handleContained(handler, _preContained[handler] = preInject(handler)); + handleContained(handler, _preContained[handler] = preInject(handler)); return await h(req, res); // return await closureMirror.apply(args).reflectee; } /// Use [sendResponse] instead. @deprecated - Future sendRequest( - HttpRequest request, RequestContext req, ResponseContext res) => + Future sendRequest(HttpRequest request, RequestContext req, + ResponseContext res) => sendResponse(request, req, res); /// Sends a response. - Future sendResponse( - HttpRequest request, RequestContext req, ResponseContext res, + Future sendResponse(HttpRequest request, RequestContext req, + ResponseContext res, {bool ignoreFinalizers: false}) { _afterProcessed.add(request); @@ -503,7 +524,7 @@ class Angel extends AngelBase { Future finalizers = ignoreFinalizers == true ? new Future.value() : responseFinalizers.fold( - new Future.value(), (out, f) => out.then((_) => f(req, res))); + new Future.value(), (out, f) => out.then((_) => f(req, res))); if (res.isOpen) res.end(); @@ -515,10 +536,40 @@ class Angel extends AngelBase { ..chunkedTransferEncoding = res.chunked ?? true ..set(HttpHeaders.CONTENT_LENGTH, res.buffer.length); + List outputBuffer = res.buffer.toBytes(); + + if (res.encoders.isNotEmpty) { + var allowedEncodings = + (req.headers[HttpHeaders.ACCEPT_ENCODING] ?? []).map((str) { + // Ignore quality specifications in accept-encoding + // ex. gzip;q=0.8 + if (!str.contains(';')) return str; + return str.split(';')[0]; + }); + + for (var encodingName in allowedEncodings) { + Converter, List> encoder; + String key = encodingName; + + if (res.encoders.containsKey(encodingName)) + encoder = res.encoders[encodingName]; + else if (encodingName == '*') { + encoder = res.encoders[key = res.encoders.keys.first]; + } + + if (encoder != null) { + request.response.headers + .set(HttpHeaders.CONTENT_ENCODING, key); + outputBuffer = res.encoders[key].convert(outputBuffer); + break; + } + } + } + request.response ..statusCode = res.statusCode ..cookies.addAll(res.cookies) - ..add(res.buffer.toBytes()); + ..add(outputBuffer); return finalizers.then((_) => request.response.close()); } @@ -529,7 +580,9 @@ class Angel extends AngelBase { await configurer(this); if (configurer is Controller) - _onController.add(controllers[configurer.findExpose().path] = configurer); + _onController.add(controllers[configurer + .findExpose() + .path] = configurer); } /// Starts the server, wrapped in a [runZoned] call. @@ -633,8 +686,9 @@ class Angel extends AngelBase { /// An instance mounted on a server started by the [serverGenerator]. factory Angel.custom(ServerGenerator serverGenerator, - {@deprecated bool debug: false}) => - new Angel().._serverGenerator = serverGenerator; + {@deprecated bool debug: false}) => + new Angel() + .._serverGenerator = serverGenerator; factory Angel.fromSecurityContext(SecurityContext context, {@deprecated bool debug: false}) { @@ -655,7 +709,7 @@ class Angel extends AngelBase { factory Angel.secure(String certificateChainPath, String serverKeyPath, {bool debug: false, String password}) { var certificateChain = - Platform.script.resolve(certificateChainPath).toFilePath(); + Platform.script.resolve(certificateChainPath).toFilePath(); var serverKey = Platform.script.resolve(serverKeyPath).toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain, password: password); diff --git a/pubspec.yaml b/pubspec.yaml index 0a336817..0be3cf64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.8 +version: 1.0.9 description: A high-powered HTTP server with DI, routing and more. author: Tobe O homepage: https://github.com/angel-dart/angel_framework @@ -9,6 +9,7 @@ dependencies: angel_model: ^1.0.0 angel_route: ">=1.0.5 <2.0.0" body_parser: ^1.0.0-dev + charcode: ^1.0.0 container: ^0.1.2 flatten: ^1.0.0 json_god: ^2.0.0-beta @@ -17,6 +18,7 @@ dependencies: mime: ^0.9.3 random_string: ^0.0.1 dev_dependencies: + matcher: ^0.12.0 mock_request: ^1.0.0 http: ^0.11.3 test: ^0.12.13 diff --git a/test/all.dart b/test/all.dart index 7cdfd9e4..f2d089d6 100644 --- a/test/all.dart +++ b/test/all.dart @@ -2,6 +2,7 @@ import 'accepts_test.dart' as accepts; import 'anonymous_service_test.dart' as anonymous_service; import 'controller_test.dart' as controller; import 'di_test.dart' as di; +import 'encoders_buffer_test.dart' as encoders_buffer; import 'exception_test.dart' as exception; import 'general_test.dart' as general; import 'hooked_test.dart' as hooked; @@ -10,6 +11,7 @@ import 'routing_test.dart' as routing; import 'serialize_test.dart' as serialize; import 'server_test.dart' as server; import 'services_test.dart' as services; +import 'streaming_test.dart' as streaming; import 'typed_service_test.dart' as typed_service; import 'util_test.dart' as util; import 'view_generator_test.dart' as view_generator; @@ -21,6 +23,7 @@ main() { group('anonymous service', anonymous_service.main); group('controller', controller.main); group('di', di.main); + group('encoders_buffer', encoders_buffer.main); group('exception', exception.main); group('general', general.main); group('hooked', hooked.main); @@ -29,6 +32,7 @@ main() { group('serialize', serialize.main); group('server', server.main); group('services', services.main); + group('streaming', streaming.main); group('typed_service', typed_service.main); group('util', util.main); group('view generator', view_generator.main); diff --git a/test/controller_test.dart b/test/controller_test.dart index e56000ae..7c371790 100644 --- a/test/controller_test.dart +++ b/test/controller_test.dart @@ -44,7 +44,7 @@ main() { String url; setUp(() async { - app = new Angel(debug: true); + app = new Angel(); app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); app.registerMiddleware("bar", (req, res) async => res.write("world!")); app.get( diff --git a/test/di_test.dart b/test/di_test.dart index 9f441c13..3882a750 100644 --- a/test/di_test.dart +++ b/test/di_test.dart @@ -16,7 +16,7 @@ main() { String url; setUp(() async { - app = new Angel(debug: true); + app = new Angel(); client = new http.Client(); // Inject some todos diff --git a/test/encoders_buffer_test.dart b/test/encoders_buffer_test.dart new file mode 100644 index 00000000..47ed8812 --- /dev/null +++ b/test/encoders_buffer_test.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:mock_request/mock_request.dart'; +import 'package:test/test.dart'; + +main() async { + Angel app; + + setUp(() { + app = new Angel(); + app.injectEncoders( + { + 'deflate': ZLIB.encoder, + 'gzip': GZIP.encoder, + }, + ); + + app.get('/hello', (res) { + res.write('Hello, world!'); + }); + }); + + tearDown(() => app.close()); + + encodingTests(() => app); +} + +void encodingTests(Angel getApp()) { + group('encoding', () { + Angel app; + + setUp(() { + app = getApp(); + }); + + test('sends plaintext if no accept-encoding', () async { + var rq = new MockHttpRequest('GET', Uri.parse('/hello'))..close(); + var rs = rq.response; + await app.handleRequest(rq); + + var body = await rs.transform(UTF8.decoder).join(); + expect(body, 'Hello, world!'); + }); + + test('encodes if wildcard', () async { + var rq = new MockHttpRequest('GET', Uri.parse('/hello')) + ..headers.set(HttpHeaders.ACCEPT_ENCODING, '*') + ..close(); + var rs = rq.response; + await app.handleRequest(rq); + + var body = await rs.fold>([], (out, list) => []..addAll(list)); + expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'deflate'); + expect(body, ZLIB.encode(UTF8.encode('Hello, world!'))); + }); + + test('encodes if wildcard + multiple', () async { + var rq = new MockHttpRequest('GET', Uri.parse('/hello')) + ..headers.set(HttpHeaders.ACCEPT_ENCODING, ['foo', 'bar', '*']) + ..close(); + var rs = rq.response; + await app.handleRequest(rq); + + var body = await rs.fold>([], (out, list) => []..addAll(list)); + expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'deflate'); + expect(body, ZLIB.encode(UTF8.encode('Hello, world!'))); + }); + + test('encodes if explicit', () async { + var rq = new MockHttpRequest('GET', Uri.parse('/hello')) + ..headers.set(HttpHeaders.ACCEPT_ENCODING, 'gzip') + ..close(); + var rs = rq.response; + await app.handleRequest(rq); + + var body = await rs.fold>([], (out, list) => []..addAll(list)); + expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip'); + expect(body, GZIP.encode(UTF8.encode('Hello, world!'))); + }); + + test('only uses one encoder', () async { + var rq = new MockHttpRequest('GET', Uri.parse('/hello')) + ..headers.set(HttpHeaders.ACCEPT_ENCODING, ['gzip', 'deflate']) + ..close(); + var rs = rq.response; + await app.handleRequest(rq); + + var body = await rs.fold>([], (out, list) => []..addAll(list)); + expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip'); + expect(body, GZIP.encode(UTF8.encode('Hello, world!'))); + }); + }); +} diff --git a/test/routing_test.dart b/test/routing_test.dart index e6c955de..b0074f69 100644 --- a/test/routing_test.dart +++ b/test/routing_test.dart @@ -24,10 +24,9 @@ main() { http.Client client; setUp(() async { - final debug = true; - app = new Angel(debug: debug); - nested = new Angel(debug: debug); - todos = new Angel(debug: debug); + app = new Angel(); + nested = new Angel(); + todos = new Angel(); // Lazy-parse in production [app, nested, todos].forEach((Angel app) { diff --git a/test/streaming_test.dart b/test/streaming_test.dart new file mode 100644 index 00000000..b59dc91a --- /dev/null +++ b/test/streaming_test.dart @@ -0,0 +1,91 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:mock_request/mock_request.dart'; +import 'package:test/test.dart'; +import 'encoders_buffer_test.dart' show encodingTests; + +main() async { + Angel app; + + setUp(() { + app = new Angel(); + app.injectEncoders( + { + 'deflate': ZLIB.encoder, + 'gzip': GZIP.encoder, + }, + ); + + app.get('/hello', (res) { + new Stream>.fromIterable(['Hello, world!'.codeUnits]).pipe(res); + }); + + app.get('/write', (res) async { + await res.addStream( + new Stream>.fromIterable(['Hello, world!'.codeUnits])); + res.write('bye'); + await res.close(); + }); + + app.get('/multiple', (res) async { + await res.addStream( + new Stream>.fromIterable(['Hello, world!'.codeUnits])); + await res.addStream( + new Stream>.fromIterable(['bye'.codeUnits])); + await res.close(); + }); + + app.get('/overwrite', (res) async { + res.statusCode = 32; + await new Stream>.fromIterable(['Hello, world!'.codeUnits]) + .pipe(res); + await new Stream>.fromIterable(['Hello, world!'.codeUnits]) + .pipe(res); + }); + + app.get('/error', (res) => res.addError(new StateError('wtf'))); + + app.fatalErrorStream.listen((e) { + stderr..writeln(e.error)..writeln(e.stack); + }); + }); + + tearDown(() => app.close()); + + _expectHelloBye(String path) async { + var rq = new MockHttpRequest('GET', Uri.parse(path))..close(); + await app.handleRequest(rq); + var body = await rq.response.transform(UTF8.decoder).join(); + expect(body, 'Hello, world!bye'); + } + + test('write after addStream', () => _expectHelloBye('/write')); + + test('multiple addStream', () => _expectHelloBye('/multiple')); + + test('cannot write after close', () async { + var rq = new MockHttpRequest('GET', Uri.parse('/overwrite')) + ..close(); + await app.handleRequest(rq); + var body = await rq.response.transform(UTF8.decoder).join(); + + if (rq.response.statusCode != 32) + throw 'overwrite should throw error; response: $body'; + }); + + test('res => addError', () async { + try { + var rq = new MockHttpRequest('GET', Uri.parse('/error')) + ..close(); + await app.handleRequest(rq); + var body = await rq.response.transform(UTF8.decoder).join(); + throw 'addError should throw error; response: $body'; + } on StateError { + // Should throw error... + } + }); + + encodingTests(() => app); +}