diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml index c753608c..dcd3564d 100644 --- a/.idea/runConfigurations/All_Tests.xml +++ b/.idea/runConfigurations/All_Tests.xml @@ -2,7 +2,7 @@ \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 8b7d24a9..7fda60b9 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,14 +1,15 @@ - - + + + + - - + - + @@ -32,20 +33,48 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -55,8 +84,40 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -65,27 +126,17 @@ - + - - - - - - - - - - - + @@ -176,16 +227,18 @@ @@ -219,7 +272,6 @@ - @@ -316,6 +368,7 @@ + @@ -344,7 +397,7 @@ - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -516,7 +575,8 @@ - + + 1481237183504 @@ -672,27 +732,34 @@ - - + - - + + - - + + - - + + - - + + - - + + @@ -708,7 +775,7 @@ - @@ -721,13 +788,14 @@ - + - - + + + @@ -736,14 +804,12 @@ - - @@ -775,35 +841,15 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -938,16 +984,6 @@ - - - - - - - - - - @@ -1032,13 +1068,6 @@ - - - - - - - @@ -1081,50 +1110,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1132,19 +1120,111 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f55f08bc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.7 +Added an `accepts` method to `RequestContext`. It's now a lot easier to tell which content types the +user accepts via the `Accept` header. \ No newline at end of file diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index 34f31106..73dcbd5e 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -8,6 +8,8 @@ import 'server.dart' show Angel; /// A convenience wrapper around an incoming HTTP request. class RequestContext extends Extensible { + String _acceptHeaderCache; + bool _acceptsAllCache; BodyParseResult _body; ContentType _contentType; HttpRequest _io; @@ -187,6 +189,36 @@ class RequestContext extends Extensible { injections[type] = value; } + /// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response. + /// + /// You cannot provide a `null` [contentType]. + /// If the `Accept` header's value is `*/*`, this method will always return `true`. + /// + /// [contentType] can be either of the following: + /// * A [ContentType], in which case the `Accept` header will be compared against its `mimeType` property. + /// * Any other Dart value, in which case the `Accept` header will be compared against the result of a `toString()` call. + bool accepts(contentType) { + var contentTypeString = contentType is ContentType + ? contentType.mimeType + : contentType?.toString(); + + if (contentTypeString == null) + throw new ArgumentError( + 'RequestContext.accepts expects the `contentType` parameter to NOT be null.'); + + _acceptHeaderCache ??= headers.value(HttpHeaders.ACCEPT); + + if (_acceptHeaderCache == null) + return false; + else if (_acceptHeaderCache.contains('*/*')) + return true; + else + return _acceptHeaderCache.contains(contentTypeString); + } + + /// Returns as `true` if the client's `Accept` header indicates that it will accept any response content type. + bool get acceptsAll => _acceptsAllCache ??= accepts('*/*'); + /// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the body. Future lazyBody() => parse().then((b) => b.body); diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 6322b3e2..357e3eaa 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -324,6 +324,7 @@ class Angel extends AngelBase { return parent != null ? parent.findProperty(key) : null; } + /// Handles an [AngelHttpException]. handleAngelHttpException(AngelHttpException e, StackTrace st, RequestContext req, ResponseContext res, HttpRequest request, {bool ignoreFinalizers: false}) async { @@ -339,11 +340,10 @@ class Angel extends AngelBase { } res.statusCode = e.statusCode; - List accept = request?.headers[HttpHeaders.ACCEPT] ?? ['*/*']; - if (accept.isEmpty || - accept.contains('*/*') || - accept.contains(ContentType.JSON.mimeType) || - accept.contains("application/javascript")) { + if (req.headers.value(HttpHeaders.ACCEPT) == null || + req.acceptsAll || + req.accepts(ContentType.JSON) || + req.accepts('application/javascript')) { res.serialize(e.toMap(), contentType: res.headers[HttpHeaders.CONTENT_TYPE] ?? ContentType.JSON.mimeType); diff --git a/pubspec.yaml b/pubspec.yaml index 852bea8d..eb465f9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.6+2 +version: 1.0.7 description: A high-powered HTTP server with DI, routing and more. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/accepts_test.dart b/test/accepts_test.dart new file mode 100644 index 00000000..adaf7c5b --- /dev/null +++ b/test/accepts_test.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:mock_request/mock_request.dart'; +import 'package:test/test.dart'; + +final Uri ENDPOINT = Uri.parse('http://example.com/accept'); + +main() { + test('no content type', () async { + var req = await acceptContentTypes(); + expect(req.acceptsAll, isFalse); + expect(req.accepts(ContentType.JSON), isFalse); + expect(req.accepts('application/json'), isFalse); + expect(req.accepts(ContentType.HTML), isFalse); + expect(req.accepts('text/html'), isFalse); + }); + + test('wildcard', () async { + var req = await acceptContentTypes(['*/*']); + expect(req.acceptsAll, isTrue); + expect(req.accepts(ContentType.JSON), isTrue); + expect(req.accepts('application/json'), isTrue); + expect(req.accepts(ContentType.HTML), isTrue); + expect(req.accepts('text/html'), isTrue); + }); + + test('specific type', () async { + var req = await acceptContentTypes(['text/html']); + expect(req.acceptsAll, isFalse); + expect(req.accepts(ContentType.JSON), isFalse); + expect(req.accepts('application/json'), isFalse); + expect(req.accepts(ContentType.HTML), isTrue); + expect(req.accepts('text/html'), isTrue); + }); + + group('disallow null', () { + RequestContext req; + + setUp(() async { + req = await acceptContentTypes(); + }); + + test('throws error', () { + expect(() => req.accepts(null), throwsArgumentError); + }); + }); +} + +Future acceptContentTypes( + [Iterable contentTypes = const []]) { + var headerString = contentTypes.isEmpty ? null : contentTypes.join(','); + var rq = new MockHttpRequest('GET', ENDPOINT); + rq.headers.set(HttpHeaders.ACCEPT, headerString); + rq.close(); + var app = new Angel(); + return app.createRequestContext(rq); +} diff --git a/test/all.dart b/test/all.dart index d2f04e15..7cdfd9e4 100644 --- a/test/all.dart +++ b/test/all.dart @@ -1,3 +1,4 @@ +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; @@ -16,6 +17,7 @@ import 'package:test/test.dart'; /// For running with coverage main() { + group('accepts', accepts.main); group('anonymous service', anonymous_service.main); group('controller', controller.main); group('di', di.main);