diff --git a/.gitignore b/.gitignore
index 7c280441..c216ecad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,47 @@ doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
diff --git a/.idea/runConfigurations/All_Tests__coverage_.xml b/.idea/runConfigurations/All_Tests__coverage_.xml
new file mode 100644
index 00000000..ab71df17
--- /dev/null
+++ b/.idea/runConfigurations/All_Tests__coverage_.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 291e8ecf..b8dc9cf3 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,29 @@
# shelf
-[](https://pub.dartlang.org/packages/angel_shelf)
+[](https://pub.dartlang.org/packages/angel_shelf)
[](https://travis-ci.org/angel-dart/shelf)
-Shelf interop with Angel. Will be deprecated by v2.0.0.
+Shelf interop with Angel. This package lets you run `package:shelf` handlers via a custom adapter.
+It also includes a plug-in that configures Angel to *natively* run `shelf` response handlers.
-By version 2 of Angel, I will migrate the server to run on top of `shelf`.
-Until then, use the code in this repo to embed existing shelf apps into
-your Angel applications.
+Use the code in this repo to embed existing shelf apps into
+your Angel applications. This way, you can migrate legacy applications without
+having to rewrite your business logic.
This will make it easy to layer your API over a production application,
rather than having to port code.
+* [Usage](#usage)
+ * [embedShelf](#embedshelf)
+ * [Communicating with Angel](#communicating-with-angel-with-embedshelf))
+ * [supportShelf](#supportshelf)
+ * [Communicating with Angel](#communicating-with-angel-with-supportshelf)
+
+# Usage
+
+## embedShelf
+This is a compliant `shelf` adapter that acts as an Angel request handler. You can use it as a middleware,
+or attach it to individual routes.
+
```dart
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
@@ -32,6 +45,71 @@ main() async {
.addHandler(_echoRequest)
));
+ // Only on a specific route
+ app.get('/shelf', handler);
+
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
}
```
+
+### Communicating with Angel with embedShelf
+You can communicate with Angel:
+
+```dart
+handleRequest(shelf.Request request) {
+ // Access original Angel request...
+ var req = request.context['angel_shelf.request'] as RequestContext;
+
+ // ... And then interact with it.
+ req.inject('from_shelf', new Foo());
+
+ // `req.properties` are also available.
+ var props = request.context['angel_shelf.properties'] as Map;
+
+}
+```
+
+## supportShelf
+This plug-in takes advantage of Angel's middleware system and dependency injection to patch a server
+to run `shelf` request handlers as though they were Angel request handlers. Hooray for integration!
+
+You'll want to run this before adding any other response finalizers that depend on
+the response content being effectively final, i.e. GZIP compression.
+
+**NOTE**: Do not inject a `shelf.Request` into your request under the name `req`. If you do,
+Angel will automatically inject a `RequestContext` instead.
+
+```dart
+configureServer(Angel app) async {
+ // Return a shelf Response
+ app.get('/shelf', (shelf.Request request) => new shelf.Response.ok('Yay!'));
+
+ // Return an arbitrary value.
+ //
+ // This will be serialized by Angel as per usual.
+ app.get('/json', (shelf.Request request) => {'foo': 'bar'});
+
+ // You can still throw Angel exceptions.
+ //
+ // Don't be fooled: just because this is a shelf handler, doesn't mean
+ // it's not an Angel response handler too. ;)
+ app.get('/error', (shelf.Request request) {
+ throw new AngelHttpException.forbidden();
+ });
+
+ // Make it all happen!
+ await app.configure(supportShelf());
+}
+```
+
+### Communicating with Angel with supportShelf
+The following keys will be present in the shelf request's context:
+ * `angel_shelf.request` - Original RequestContext
+ * `angel_shelf.response` - Original ResponseContext
+ * `angel_shelf.properties` - Original RequestContext's properties
+
+If the original `RequestContext` contains a Map named `shelf_context` in its `properties`,
+then it will be merged into the shelf request's context.
+
+If the handler returns a `shelf.Response`, then it will be present in `ResponseContext.properties`
+as `shelf_response`.
\ No newline at end of file
diff --git a/lib/angel_shelf.dart b/lib/angel_shelf.dart
index efa0d5a5..22e24582 100644
--- a/lib/angel_shelf.dart
+++ b/lib/angel_shelf.dart
@@ -1,13 +1,3 @@
-import 'package:angel_framework/angel_framework.dart';
-import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf/shelf_io.dart' as io;
-
-/// Simply passes an incoming request to a `shelf` handler.
-RequestHandler embedShelf(shelf.Handler handler) {
- return (RequestContext req, ResponseContext res) async {
- res
- ..willCloseItself = true
- ..end();
- io.handleRequest(req.io, handler);
- };
-}
+export 'src/convert.dart';
+export 'src/embed_shelf.dart';
+export 'src/support_shelf.dart';
\ No newline at end of file
diff --git a/lib/src/convert.dart b/lib/src/convert.dart
new file mode 100644
index 00000000..22a7ebf1
--- /dev/null
+++ b/lib/src/convert.dart
@@ -0,0 +1,65 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:shelf/shelf.dart' as shelf;
+
+/// Creates a [shelf.Request] analogous to the input [request].
+///
+/// The new request's `context` will contain [request.properties] as `angel_shelf.properties`, as well as
+/// the provided [context], if any.
+///
+/// The context will also have the original request available as `angel_shelf.request`.
+///
+/// If you want to read the request body, you *must* `storeOriginalBuffer` to `true`
+/// on your application instance.
+Future convertRequest(RequestContext request,
+ {String handlerPath, Map context}) async {
+ var headers = {};
+ request.headers.forEach((k, v) {
+ headers[k] = v.join(',');
+ });
+
+ headers.remove(HttpHeaders.TRANSFER_ENCODING);
+
+ void onHijack(
+ void hijack(Stream> stream, StreamSink> sink)) {
+ request.io.response.detachSocket(writeHeaders: false).then((socket) {
+ return request.lazyOriginalBuffer().then((buf) {
+ var ctrl = new StreamController>()..add(buf ?? []);
+ socket.listen(ctrl.add, onError: ctrl.addError, onDone: ctrl.close);
+ hijack(socket, socket);
+ });
+ }).catchError((e, st) {
+ stderr.writeln('An error occurred while hijacking a shelf request: $e');
+ stderr.writeln(st);
+ });
+ }
+
+ return new shelf.Request(request.method, request.io.requestedUri,
+ protocolVersion: request.io.protocolVersion,
+ headers: headers,
+ handlerPath: handlerPath,
+ url: new Uri(
+ path: request.io.requestedUri.path.substring(1),
+ query: request.io.requestedUri.query),
+ body: (await request.lazyOriginalBuffer()) ?? [],
+ context: {'angel_shelf.request': request}
+ ..addAll({'angel_shelf.properties': request.properties})
+ ..addAll(context ?? {}),
+ onHijack: onHijack);
+}
+
+/// Applies the state of the [shelfResponse] into the [angelResponse].
+///
+/// Merges all headers, sets the status code, and writes the body.
+///
+/// In addition, the response's context will be available in `angelResponse.properties`
+/// as `shelf_context`.
+Future mergeShelfResponse(
+ shelf.Response shelfResponse, ResponseContext angelResponse) async {
+ angelResponse.headers.addAll(shelfResponse.headers);
+ angelResponse.statusCode = shelfResponse.statusCode;
+ angelResponse.properties['shelf_context'] = shelfResponse.context;
+ await shelfResponse.read().forEach(angelResponse.buffer.add);
+ angelResponse.end();
+}
diff --git a/lib/src/embed_shelf.dart b/lib/src/embed_shelf.dart
new file mode 100644
index 00000000..fc95e3a3
--- /dev/null
+++ b/lib/src/embed_shelf.dart
@@ -0,0 +1,36 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'convert.dart';
+
+/// Simply passes an incoming request to a `shelf` handler.
+///
+/// If the handler does not return a [shelf.Response], then the
+/// result will be passed down the Angel middleware pipeline, like with
+/// any other request handler.
+///
+/// If [throwOnNullResponse] is `true` (default: `false`), then a 500 error will be thrown
+/// if the [handler] returns `null`.
+RequestHandler embedShelf(shelf.Handler handler,
+ {String handlerPath,
+ Map context,
+ bool throwOnNullResponse: false}) {
+ return (RequestContext req, ResponseContext res) async {
+ var shelfRequest =
+ await convertRequest(req, handlerPath: handlerPath, context: context);
+ try {
+ var result = await handler(shelfRequest);
+ if (result is! shelf.Response && result != null) return result;
+ if (result == null && throwOnNullResponse == true)
+ throw new AngelHttpException('Internal Server Error');
+ await mergeShelfResponse(result, res);
+ return false;
+ } on shelf.HijackException {
+ // On hijack...
+ res
+ ..willCloseItself = true
+ ..end();
+ } catch (e) {
+ rethrow;
+ }
+ };
+}
diff --git a/lib/src/support_shelf.dart b/lib/src/support_shelf.dart
new file mode 100644
index 00000000..ab6b9a33
--- /dev/null
+++ b/lib/src/support_shelf.dart
@@ -0,0 +1,40 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'convert.dart';
+
+/// Configures a server instance to natively run shelf request handlers.
+///
+/// To pass a context to the generated shelf request, add a Map
+/// to `req.properties`, named `shelf_context`.
+///
+/// Two additional keys will be present in the `shelf` request context:
+/// * `angel_shelf.request` - The Angel [RequestContext].
+/// * `angel_shelf.response` - The Angel [ResponseContext].
+AngelConfigurer supportShelf() {
+ return (Angel app) async {
+ app.before.add((RequestContext req, ResponseContext res) async {
+ // Inject shelf.Request ;)
+ req.inject(
+ shelf.Request,
+ await convertRequest(req,
+ context: {'angel_shelf.response': res}
+ ..addAll(req.properties['shelf_context'] ?? {})));
+
+ // Override serializer to support returning shelf responses
+ var oldSerializer = res.serializer;
+ res.serializer = (val) {
+ if (val is! shelf.Response) return oldSerializer(val);
+ res.properties['shelf_response'] = val;
+ return ''; // Write nothing
+ };
+ });
+
+ // Merge shelf response if necessary
+ app.responseFinalizers.add((RequestContext req, ResponseContext res) async {
+ if (res.properties.containsKey('shelf_response')) {
+ var shelfResponse = res.properties['shelf_response'] as shelf.Response;
+ await mergeShelfResponse(shelfResponse, res);
+ }
+ });
+ };
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 0f018d5b..8b5ea860 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: angel_shelf
-description: Shelf interop with Angel. Will be deprecated by v2.0.0.
-version: 1.0.0
+description: Shelf interop with Angel.
+version: 1.1.0
author: Tobe O
homepage: https://github.com/angel-dart/shelf
environment:
@@ -9,5 +9,6 @@ dependencies:
angel_framework: ^1.0.0-dev
shelf: ^0.6.0
dev_dependencies:
- angel_test: ^1.0.0-dev
+ angel_diagnostics: ^1.0.0
+ angel_test: ^1.0.0
test: ^0.12.0
\ No newline at end of file
diff --git a/test/all.dart b/test/all.dart
new file mode 100644
index 00000000..79cee043
--- /dev/null
+++ b/test/all.dart
@@ -0,0 +1,8 @@
+import 'package:test/test.dart';
+import 'embed_shelf_test.dart' as embed_shelf;
+import 'support_shelf_test.dart' as support_shelf;
+
+main() {
+ group('embed_shelf', embed_shelf.main);
+ group('support_shelf', support_shelf.main);
+}
\ No newline at end of file
diff --git a/test/all_test.dart b/test/all_test.dart
deleted file mode 100644
index d03ce8cc..00000000
--- a/test/all_test.dart
+++ /dev/null
@@ -1,41 +0,0 @@
-import 'dart:convert';
-import 'package:angel_framework/angel_framework.dart';
-import 'package:angel_shelf/angel_shelf.dart';
-import 'package:angel_test/angel_test.dart';
-import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf/shelf_io.dart' as io;
-import 'package:test/test.dart';
-
-main() async {
- Angel app;
- TestClient client;
-
- setUp(() async {
- var handler = new shelf.Pipeline()
- .addMiddleware(shelf.logRequests())
- .addHandler(_echoRequest);
-
- app = new Angel();
- app.get('/angel', 'Angel');
- app.after.add(embedShelf(handler));
-
- client = await connectTo(app);
- });
-
- tearDown(() => client.close());
-
- test('expose angel side', () async {
- var response = await client.get('/angel');
- expect(JSON.decode(response.body), equals('Angel'));
- });
-
- test('expose shelf side', () async {
- var response = await client.get('/foo');
- expect(response, hasStatus(200));
- expect(response.body, equals('Request for "foo"'));
- });
-}
-
-shelf.Response _echoRequest(shelf.Request request) {
- return new shelf.Response.ok('Request for "${request.url}"');
-}
diff --git a/test/embed_shelf_test.dart b/test/embed_shelf_test.dart
new file mode 100644
index 00000000..775978ee
--- /dev/null
+++ b/test/embed_shelf_test.dart
@@ -0,0 +1,99 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_client/io.dart' as c;
+import 'package:angel_diagnostics/angel_diagnostics.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_shelf/angel_shelf.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:charcode/charcode.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/test.dart';
+
+main() {
+ c.Angel client;
+ HttpServer server;
+ String url;
+
+ setUp(() async {
+ var handler = new shelf.Pipeline().addHandler((shelf.Request request) {
+ if (request.url.path == 'two')
+ return 2;
+ else if (request.url.path == 'error')
+ throw new AngelHttpException.notFound();
+ else if (request.url.path == 'status')
+ return new shelf.Response.notModified(headers: {'foo': 'bar'});
+ else if (request.url.path == 'hijack') {
+ request.hijack((Stream> stream, StreamSink> sink) {
+ sink.add(UTF8.encode('HTTP/1.1 200 OK\r\n'));
+ sink.add([$lf]);
+ sink.add(UTF8.encode(JSON.encode({'error': 'crime'})));
+ sink.close();
+ });
+ } else if (request.url.path == 'throw')
+ return null;
+ else
+ return new shelf.Response.ok('Request for "${request.url}"');
+ });
+
+ var app = new Angel()..lazyParseBodies = true;
+ app.get('/angel', 'Angel');
+ app.after.add(embedShelf(handler, throwOnNullResponse: true));
+ await app.configure(logRequests());
+
+ server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
+ client =
+ new c.Rest(url = 'http://${server.address.address}:${server.port}');
+ });
+
+ tearDown(() async {
+ await client.close();
+ await server.close(force: true);
+ });
+
+ test('expose angel side', () async {
+ var response = await client.get('/angel');
+ expect(JSON.decode(response.body), equals('Angel'));
+ });
+
+ test('expose shelf side', () async {
+ var response = await client.get('/foo');
+ expect(response, hasStatus(200));
+ expect(response.body, equals('Request for "foo"'));
+ });
+
+ test('shelf can return arbitrary values', () async {
+ var response = await client.get('/two');
+ expect(response, isJson(2));
+ });
+
+ test('shelf can hijack', () async {
+ try {
+ var client = new HttpClient();
+ var rq = await client.openUrl('GET', Uri.parse('$url/hijack'));
+ var rs = await rq.close();
+ var body = await rs.transform(UTF8.decoder).join();
+ print('Response: $body');
+ expect(JSON.decode(body), {'error': 'crime'});
+ } on HttpException catch (e, st) {
+ print('HTTP Exception: ' + e.message);
+ print(st);
+ rethrow;
+ }
+ });
+
+ test('shelf can set status code', () async {
+ var response = await client.get('/status');
+ expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar')));
+ });
+
+ test('shelf can throw error', () async {
+ var response = await client.get('/error');
+ expect(response, hasStatus(404));
+ });
+
+ test('throw on null', () async {
+ var response = await client.get('/throw');
+ expect(response, hasStatus(500));
+ });
+}
diff --git a/test/support_shelf_test.dart b/test/support_shelf_test.dart
new file mode 100644
index 00000000..5f641b7a
--- /dev/null
+++ b/test/support_shelf_test.dart
@@ -0,0 +1,41 @@
+import 'package:angel_diagnostics/angel_diagnostics.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_shelf/angel_shelf.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/test.dart';
+
+main() {
+ TestClient client;
+
+ setUp(() async {
+ var app = new Angel()..lazyParseBodies = true;
+
+ app.get('/inject', (shelf.Request request) {
+ print('URL of injected request: ${request.url.path}');
+ return {'inject': request.url.path == 'inject'};
+ });
+
+ app.get('/hello', (shelf.Request request) {
+ return new shelf.Response.ok('world');
+ });
+
+ await app.configure(supportShelf());
+ await app.configure(logRequests());
+ client = await connectTo(app);
+ });
+
+ tearDown(() => client.close());
+
+ test('injected into request', () async {
+ var response = await client.get('/inject');
+ print('Response: ${response.body}');
+ expect(response, isJson({'inject': true}));
+ });
+
+ test('can return shelf response', () async {
+ var response = await client.get('/hello');
+ print('Response: ${response.body}');
+ expect(response, hasBody('world'));
+ });
+}