diff --git a/packages/shelf/.gitignore b/packages/shelf/.gitignore
new file mode 100644
index 00000000..8fff3906
--- /dev/null
+++ b/packages/shelf/.gitignore
@@ -0,0 +1,72 @@
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.buildlog
+.packages
+.project
+.pub/
+build/
+**/packages/
+
+# Files created by dart2js
+# (Most Dart developers will use pub build to compile Dart, use/modify these
+# rules if you intend to use dart2js directly
+# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
+# differentiate from explicit Javascript files)
+*.dart.js
+*.part.js
+*.js.deps
+*.js.map
+*.info.json
+
+# Directory created by dartdoc
+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
+.dart_tool
\ No newline at end of file
diff --git a/packages/shelf/.idea/modules.xml b/packages/shelf/.idea/modules.xml
new file mode 100644
index 00000000..c39dfcd7
--- /dev/null
+++ b/packages/shelf/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/shelf/.idea/runConfigurations/All_Tests__coverage_.xml b/packages/shelf/.idea/runConfigurations/All_Tests__coverage_.xml
new file mode 100644
index 00000000..ab71df17
--- /dev/null
+++ b/packages/shelf/.idea/runConfigurations/All_Tests__coverage_.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/shelf/.idea/runConfigurations/tests_in_shelf.xml b/packages/shelf/.idea/runConfigurations/tests_in_shelf.xml
new file mode 100644
index 00000000..a6105b27
--- /dev/null
+++ b/packages/shelf/.idea/runConfigurations/tests_in_shelf.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/shelf/.idea/shelf.iml b/packages/shelf/.idea/shelf.iml
new file mode 100644
index 00000000..eae13016
--- /dev/null
+++ b/packages/shelf/.idea/shelf.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/shelf/.idea/vcs.xml b/packages/shelf/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/packages/shelf/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/shelf/.travis.yml b/packages/shelf/.travis.yml
new file mode 100644
index 00000000..de2210c9
--- /dev/null
+++ b/packages/shelf/.travis.yml
@@ -0,0 +1 @@
+language: dart
\ No newline at end of file
diff --git a/packages/shelf/CHANGELOG.md b/packages/shelf/CHANGELOG.md
new file mode 100644
index 00000000..70ea8a9e
--- /dev/null
+++ b/packages/shelf/CHANGELOG.md
@@ -0,0 +1,9 @@
+# 2.1.0
+* `pedantic` lints.
+* Add the `AngelShelf` driver class, which allows you to embed Angel within shelf.
+
+# 2.0.0
+* Removed `supportShelf`.
+
+# 1.2.0
+* Upgraded for `>=1.1.0` compatibility.
\ No newline at end of file
diff --git a/packages/shelf/LICENSE b/packages/shelf/LICENSE
new file mode 100644
index 00000000..15fe44bd
--- /dev/null
+++ b/packages/shelf/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 The Angel Framework
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/shelf/README.md b/packages/shelf/README.md
new file mode 100644
index 00000000..5882530a
--- /dev/null
+++ b/packages/shelf/README.md
@@ -0,0 +1,105 @@
+# shelf
+[![Pub](https://img.shields.io/pub/v/angel_shelf.svg)](https://pub.dartlang.org/packages/angel_shelf)
+[![build status](https://travis-ci.org/angel-dart/shelf.svg)](https://travis-ci.org/angel-dart/shelf)
+
+Shelf interop with Angel. This package lets you run `package:shelf` handlers via a custom adapter.
+
+Use the code in this repo to embed existing Angel/shelf apps into
+other Angel/shelf 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)
+ - [`AngelShelf`](#angelshelf)
+
+# 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';
+import 'package:angel_shelf/angel_shelf.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'api/api.dart';
+
+main() async {
+ var app = Angel();
+ var http = AngelHttp(app);
+
+ // Angel routes on top
+ await app.mountController();
+
+ // Re-route all other traffic to an
+ // existing application.
+ app.fallback(embedShelf(
+ shelf.Pipeline()
+ .addMiddleware(shelf.logRequests())
+ .addHandler(_echoRequest)
+ ));
+
+ // Or, only on a specific route:
+ app.get('/shelf', wrappedShelfHandler);
+
+ await http.startServer(InternetAddress.loopbackIPV4, 3000);
+ print(http.uri);
+}
+```
+
+### 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.container.registerNamedSingleton('from_shelf', Foo());
+
+ // `req.container` is also available.
+ var container = request.context['angel_shelf.container'] as Container;
+ container.make().drive();
+}
+```
+
+### AngelShelf
+Angel 2 brought about the generic `Driver` class, which is implemented
+by `AngelHttp`, `AngelHttp2`, `AngelGopher`, etc., and provides the core
+infrastructure for request handling in Angel.
+
+`AngelShelf` is an implementation that wraps shelf requests and responses in their
+Angel equivalents. Using it is as simple using as using `AngelHttp`, or any other
+driver:
+
+```dart
+// Create an AngelShelf driver.
+// If we have startup hooks we want to run, we need to call
+// `startServer`. Otherwise, it can be omitted.
+// Of course, if you call `startServer`, know that to run
+// shutdown/cleanup logic, you need to call `close` eventually,
+// too.
+var angelShelf = AngelShelf(app);
+await angelShelf.startServer();
+
+await shelf_io.serve(angelShelf.handler, InternetAddress.loopbackIPv4, 8081);
+```
+
+You can also use the `AngelShelf` driver as a shelf middleware - just use
+`angelShelf.middleware` instead of `angelShelf.handler`. When used as a middleware,
+if the Angel response context is still open after all handlers run (i.e. no routes were
+matched), the next shelf handler will be called.
+
+```dart
+var handler = shelf.Pipeline()
+ .addMiddleware(angelShelf.middleware)
+ .addHandler(createStaticHandler(...));
+```
\ No newline at end of file
diff --git a/packages/shelf/analysis_options.yaml b/packages/shelf/analysis_options.yaml
new file mode 100644
index 00000000..a4f33350
--- /dev/null
+++ b/packages/shelf/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:pedantic/analysis_options.yaml
+analyzer:
+ strong-mode:
+ implicit-casts: false
diff --git a/packages/shelf/example/angel_in_shelf.dart b/packages/shelf/example/angel_in_shelf.dart
new file mode 100644
index 00000000..808bccf2
--- /dev/null
+++ b/packages/shelf/example/angel_in_shelf.dart
@@ -0,0 +1,53 @@
+import 'dart:io';
+import 'package:angel_container/mirrors.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_shelf/angel_shelf.dart';
+import 'package:logging/logging.dart';
+import 'package:pretty_logging/pretty_logging.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_static/shelf_static.dart';
+
+main() async {
+ Logger.root
+ ..level = Level.ALL
+ ..onRecord.listen(prettyLog);
+
+ // Create a basic Angel server, with some routes.
+ var app = Angel(
+ logger: Logger('angel_shelf_demo'),
+ reflector: MirrorsReflector(),
+ );
+
+ app.get('/angel', (req, res) {
+ res.write('Angel embedded within shelf!');
+ return false;
+ });
+
+ app.get('/hello', ioc((@Query('name') String name) {
+ return {'hello': name};
+ }));
+
+ // Next, create an AngelShelf driver.
+ //
+ // If we have startup hooks we want to run, we need to call
+ // `startServer`. Otherwise, it can be omitted.
+ // Of course, if you call `startServer`, know that to run
+ // shutdown/cleanup logic, you need to call `close` eventually,
+ // too.
+ var angelShelf = AngelShelf(app);
+ await angelShelf.startServer();
+
+ // Create, and mount, a shelf pipeline...
+ // You can also embed Angel as a middleware...
+ var mwHandler = shelf.Pipeline()
+ .addMiddleware(angelShelf.middleware)
+ .addHandler(createStaticHandler('.',
+ defaultDocument: 'index.html', listDirectories: true));
+
+ // Run the servers.
+ await shelf_io.serve(mwHandler, InternetAddress.loopbackIPv4, 8080);
+ await shelf_io.serve(angelShelf.handler, InternetAddress.loopbackIPv4, 8081);
+ print('Angel as middleware: http://localhost:8080');
+ print('Angel as only handler: http://localhost:8081');
+}
diff --git a/packages/shelf/example/main.dart b/packages/shelf/example/main.dart
new file mode 100644
index 00000000..6a7b0fb5
--- /dev/null
+++ b/packages/shelf/example/main.dart
@@ -0,0 +1,35 @@
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:angel_shelf/angel_shelf.dart';
+import 'package:logging/logging.dart';
+import 'package:pretty_logging/pretty_logging.dart';
+import 'package:shelf_static/shelf_static.dart';
+
+main() async {
+ Logger.root
+ ..level = Level.ALL
+ ..onRecord.listen(prettyLog);
+
+ var app = Angel(logger: Logger('angel_shelf_demo'));
+ var http = AngelHttp(app);
+
+ // `shelf` request handler
+ var shelfHandler = createStaticHandler('.',
+ defaultDocument: 'index.html', listDirectories: true);
+
+ // Use `embedShelf` to adapt a `shelf` handler for use within Angel.
+ var wrappedHandler = embedShelf(shelfHandler);
+
+ // A normal Angel route.
+ app.get('/angel', (req, ResponseContext res) {
+ res.write('Hooray for `package:angel_shelf`!');
+ return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to.
+ });
+
+ // Pass any other request through to the static file handler
+ app.fallback(wrappedHandler);
+
+ await http.startServer(InternetAddress.loopbackIPv4, 8080);
+ print('Running at ${http.uri}');
+}
diff --git a/packages/shelf/lib/angel_shelf.dart b/packages/shelf/lib/angel_shelf.dart
new file mode 100644
index 00000000..e251ade2
--- /dev/null
+++ b/packages/shelf/lib/angel_shelf.dart
@@ -0,0 +1,5 @@
+export 'src/convert.dart';
+export 'src/embed_shelf.dart';
+export 'src/shelf_driver.dart';
+export 'src/shelf_request.dart';
+export 'src/shelf_response.dart';
diff --git a/packages/shelf/lib/src/convert.dart b/packages/shelf/lib/src/convert.dart
new file mode 100644
index 00000000..da418a67
--- /dev/null
+++ b/packages/shelf/lib/src/convert.dart
@@ -0,0 +1,86 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:stream_channel/stream_channel.dart';
+
+/// Creates a [shelf.Request] analogous to the input [req].
+///
+/// The request's `context` will contain [req.container] as `angel_shelf.container`, 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* set `keepRawRequestBuffers` to `true`
+/// on your application instance.
+Future convertRequest(RequestContext req, ResponseContext res,
+ {String handlerPath, Map context}) async {
+ var app = req.app;
+ var headers = {};
+ req.headers.forEach((k, v) {
+ headers[k] = v.join(',');
+ });
+
+ headers.remove(HttpHeaders.transferEncodingHeader);
+
+ void Function(void Function(StreamChannel>)) onHijack;
+ String protocolVersion;
+ Uri requestedUri;
+
+ protocolVersion = '1.1';
+ requestedUri = Uri.parse('http://${req.hostname}');
+ requestedUri = requestedUri.replace(path: req.uri.path);
+
+ onHijack = (void Function(StreamChannel> channel) hijack) async {
+ try {
+ print('a');
+ await res.detach();
+ print('b');
+ var ctrl = StreamChannelController>();
+ if (req.hasParsedBody) {
+ req.body.listen(ctrl.local.sink.add,
+ onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close);
+ } else {
+ await ctrl.local.sink.close();
+ }
+ scheduleMicrotask(() => ctrl.local.stream.pipe(res));
+ hijack(ctrl.foreign);
+ } catch (e, st) {
+ app.logger
+ ?.severe('An error occurred while hijacking a shelf request', e, st);
+ }
+ };
+
+ var url = req.uri;
+
+ if (p.isAbsolute(url.path)) {
+ url = url.replace(path: url.path.substring(1));
+ }
+
+ return shelf.Request(req.method, requestedUri,
+ protocolVersion: protocolVersion,
+ headers: headers,
+ handlerPath: handlerPath,
+ url: url,
+ body: req.body,
+ context: {'angel_shelf.request': req}
+ ..addAll({'angel_shelf.container': req.container})
+ ..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) {
+ angelResponse.headers.addAll(shelfResponse.headers);
+ angelResponse.statusCode = shelfResponse.statusCode;
+ angelResponse.properties['shelf_context'] = shelfResponse.context;
+ angelResponse.properties['shelf_response'] = shelfResponse;
+ return shelfResponse.read().pipe(angelResponse);
+}
diff --git a/packages/shelf/lib/src/embed_shelf.dart b/packages/shelf/lib/src/embed_shelf.dart
new file mode 100644
index 00000000..9b42642c
--- /dev/null
+++ b/packages/shelf/lib/src/embed_shelf.dart
@@ -0,0 +1,35 @@
+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, res,
+ 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 AngelHttpException('Internal Server Error');
+ }
+ await mergeShelfResponse(result, res);
+ return false;
+ } on shelf.HijackException {
+ // On hijack, do nothing, because the hijack handlers already call res.detach();
+ return null;
+ } catch (e) {
+ rethrow;
+ }
+ };
+}
diff --git a/packages/shelf/lib/src/shelf_driver.dart b/packages/shelf/lib/src/shelf_driver.dart
new file mode 100644
index 00000000..b0d47aa9
--- /dev/null
+++ b/packages/shelf/lib/src/shelf_driver.dart
@@ -0,0 +1,149 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'shelf_request.dart';
+import 'shelf_response.dart';
+
+class AngelShelf extends Driver, ShelfRequestContext, ShelfResponseContext> {
+ final StreamController incomingRequests = StreamController();
+
+ final FutureOr Function() notFound;
+
+ AngelShelf(Angel app, {FutureOr Function() notFound})
+ : this.notFound =
+ notFound ?? (() => shelf.Response.notFound('Not Found')),
+ super(app, null, useZone: false) {
+ // Inject a final handler that will keep responses open, if we are using the
+ // driver as a middleware.
+ app.fallback((req, res) {
+ if (res is ShelfResponseContext) {
+ res.closeSilently();
+ }
+ return true;
+ });
+ }
+
+ Future> close() {
+ incomingRequests.close();
+ return super.close();
+ }
+
+ Future> Function(dynamic, int) get serverGenerator =>
+ (_, __) async => incomingRequests.stream;
+
+ static UnsupportedError _unsupported() => UnsupportedError(
+ 'AngelShelf cannot mount a standalone server, or return a URI.');
+
+ Future handler(shelf.Request request) async {
+ var response = ShelfResponseContext(app);
+ var result = await handleRawRequest(request, response);
+ if (result is shelf.Response) {
+ return result;
+ } else if (!response.isOpen) {
+ return response.shelfResponse;
+ } else {
+ // return await handler(request);
+ return notFound();
+ }
+ }
+
+ shelf.Handler middleware(shelf.Handler handler) {
+ return (request) async {
+ var response = ShelfResponseContext(app);
+ var result = await handleRawRequest(request, response);
+ if (result is shelf.Response) {
+ return result;
+ } else if (!response.isOpen) {
+ return response.shelfResponse;
+ } else {
+ return await handler(request);
+ }
+ };
+ }
+
+ @override
+ Future handleAngelHttpException(
+ AngelHttpException e,
+ StackTrace st,
+ RequestContext req,
+ ResponseContext res,
+ shelf.Request request,
+ ShelfResponseContext response,
+ {bool ignoreFinalizers = false}) async {
+ await super.handleAngelHttpException(e, st, req, res, request, response,
+ ignoreFinalizers: ignoreFinalizers);
+ return response.shelfResponse;
+ }
+
+ @override
+ void addCookies(ShelfResponseContext response, Iterable cookies) {
+ // Don't do anything here, otherwise you get duplicate cookies.
+ // response.cookies.addAll(cookies);
+ }
+
+ @override
+ Future closeResponse(ShelfResponseContext response) {
+ return response.close();
+ }
+
+ @override
+ Uri get uri => throw _unsupported();
+
+ static final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
+
+ @override
+ Future createRequestContext(
+ shelf.Request request, ShelfResponseContext response) {
+ var path = request.url.path.replaceAll(_straySlashes, '');
+ if (path.isEmpty) path = '/';
+ var rq =
+ ShelfRequestContext(app, app.container.createChild(), request, path);
+ return Future.value(rq);
+ }
+
+ @override
+ Future createResponseContext(
+ shelf.Request request, ShelfResponseContext response,
+ [ShelfRequestContext correspondingRequest]) {
+ // Return the original response.
+ return Future.value(response..correspondingRequest = correspondingRequest);
+ }
+
+ @override
+ Stream createResponseStreamFromRawRequest(
+ shelf.Request request) {
+ return Stream.fromIterable([null]);
+ }
+
+ @override
+ void setChunkedEncoding(ShelfResponseContext response, bool value) {
+ response.chunked = value;
+ }
+
+ @override
+ void setContentLength(ShelfResponseContext response, int length) {
+ response.contentLength = length;
+ }
+
+ @override
+ void setHeader(ShelfResponseContext response, String key, String value) {
+ response.headers[key] = value;
+ }
+
+ @override
+ void setStatusCode(ShelfResponseContext response, int value) {
+ response.statusCode = value;
+ }
+
+ @override
+ void writeStringToResponse(ShelfResponseContext response, String value) {
+ response.write(value);
+ }
+
+ @override
+ void writeToResponse(ShelfResponseContext response, List data) {
+ response.add(data);
+ }
+}
diff --git a/packages/shelf/lib/src/shelf_request.dart b/packages/shelf/lib/src/shelf_request.dart
new file mode 100644
index 00000000..fffd6223
--- /dev/null
+++ b/packages/shelf/lib/src/shelf_request.dart
@@ -0,0 +1,77 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:angel_container/angel_container.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:mock_request/mock_request.dart';
+import 'package:shelf/shelf.dart' as shelf;
+
+class ShelfRequestContext extends RequestContext {
+ @override
+ final Angel app;
+ @override
+ final Container container;
+ @override
+ final shelf.Request rawRequest;
+ @override
+ final String path;
+
+ List _cookies;
+
+ @override
+ final MockHttpHeaders headers = MockHttpHeaders();
+
+ ShelfRequestContext(this.app, this.container, this.rawRequest, this.path) {
+ rawRequest.headers.forEach(headers.add);
+ }
+
+ @override
+ Stream> get body => rawRequest.read();
+
+ @override
+ List get cookies {
+ if (_cookies == null) {
+ // Parse cookies
+ _cookies = [];
+ var value = headers.value('cookie');
+ if (value != null) {
+ var cookieStrings = value.split(';').map((s) => s.trim());
+
+ for (var cookieString in cookieStrings) {
+ try {
+ _cookies.add(Cookie.fromSetCookieValue(cookieString));
+ } catch (_) {
+ // Ignore malformed cookies, and just don't add them to the container.
+ }
+ }
+ }
+ }
+ return _cookies;
+ }
+
+ @override
+ String get hostname => rawRequest.headers['host'];
+
+ @override
+ String get method {
+ if (!app.allowMethodOverrides) {
+ return originalMethod;
+ } else {
+ return headers.value('x-http-method-override')?.toUpperCase() ??
+ originalMethod;
+ }
+ }
+
+ @override
+ String get originalMethod => rawRequest.method;
+
+ @override
+ // TODO: implement remoteAddress
+ InternetAddress get remoteAddress => null;
+
+ @override
+ // TODO: implement session
+ HttpSession get session => null;
+
+ @override
+ Uri get uri => rawRequest.url;
+}
diff --git a/packages/shelf/lib/src/shelf_response.dart b/packages/shelf/lib/src/shelf_response.dart
new file mode 100644
index 00000000..c3d180a2
--- /dev/null
+++ b/packages/shelf/lib/src/shelf_response.dart
@@ -0,0 +1,140 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'shelf_request.dart';
+
+class ShelfResponseContext extends ResponseContext {
+ final Angel app;
+ final StreamController> _ctrl = StreamController();
+ bool _isOpen = true,
+ _isDetached = false,
+ _wasClosedByHandler = false,
+ _handlersAreDone = false;
+
+ ShelfResponseContext(this.app);
+
+ ShelfRequestContext _correspondingRequest;
+
+ bool get wasClosedByHandler => _wasClosedByHandler;
+
+ void closeSilently() => _handlersAreDone = true;
+
+ ShelfRequestContext get correspondingRequest => _correspondingRequest;
+
+ set correspondingRequest(ShelfRequestContext value) {
+ if (_correspondingRequest == null) {
+ _correspondingRequest = value;
+ } else {
+ throw StateError('The corresponding request has already been assigned.');
+ }
+ }
+
+ shelf.Response get shelfResponse {
+ return shelf.Response(statusCode, body: _ctrl.stream, headers: headers);
+ }
+
+ @override
+ Future close() {
+ if (!_handlersAreDone) {
+ _isOpen = false;
+ }
+ _ctrl.close();
+ return super.close();
+ }
+
+ Iterable __allowedEncodings;
+
+ Iterable get _allowedEncodings {
+ return __allowedEncodings ??= correspondingRequest.headers
+ .value('accept-encoding')
+ ?.split(',')
+ ?.map((s) => s.trim())
+ ?.where((s) => s.isNotEmpty)
+ ?.map((str) {
+ // Ignore quality specifications in accept-encoding
+ // ex. gzip;q=0.8
+ if (!str.contains(';')) return str;
+ return str.split(';')[0];
+ });
+ }
+
+ @override
+ Future addStream(Stream> stream) {
+ if (!isOpen && isBuffered) throw ResponseContext.closed();
+ Stream> output = stream;
+
+ if (encoders.isNotEmpty && correspondingRequest != null) {
+ if (_allowedEncodings != null) {
+ 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) {
+ output = encoders[key].bind(output);
+ break;
+ }
+ }
+ }
+ }
+
+ return _ctrl.addStream(output);
+ }
+
+ @override
+ void add(List data) {
+ if (!isOpen && isBuffered) {
+ throw ResponseContext.closed();
+ } else if (_isOpen) {
+ if (encoders.isNotEmpty && correspondingRequest != null) {
+ if (_allowedEncodings != null) {
+ 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) {
+ data = encoders[key].convert(data);
+ break;
+ }
+ }
+ }
+ }
+
+ _ctrl.add(data);
+ }
+ }
+
+ @override
+ BytesBuilder get buffer => null;
+
+ @override
+ FutureOr detach() {
+ _isDetached = true;
+ return this;
+ }
+
+ @override
+ bool get isBuffered => false;
+
+ @override
+ bool get isOpen => _isOpen && !_isDetached;
+
+ @override
+ void useBuffer() {}
+
+ @override
+ ShelfResponseContext get rawResponse => this;
+}
diff --git a/packages/shelf/pubspec.yaml b/packages/shelf/pubspec.yaml
new file mode 100644
index 00000000..d4c5d66b
--- /dev/null
+++ b/packages/shelf/pubspec.yaml
@@ -0,0 +1,18 @@
+author: Tobe O
+description: Shelf interop with Angel. Use this to wrap existing server code.
+homepage: https://github.com/angel-dart/shelf
+name: angel_shelf
+version: 2.1.0
+dependencies:
+ angel_framework: ^2.0.0-alpha
+ path: ^1.0.0
+ shelf: ^0.7.0
+ stream_channel: ^1.0.0
+dev_dependencies:
+ angel_test: ^2.0.0-alpha
+ pedantic: ^1.0.0
+ pretty_logging: ^1.0.0
+ shelf_static: ^0.2.8
+ test: ^1.0.0
+environment:
+ sdk: ">=2.0.0-dev <3.0.0"
diff --git a/packages/shelf/test/all.dart b/packages/shelf/test/all.dart
new file mode 100644
index 00000000..136a7210
--- /dev/null
+++ b/packages/shelf/test/all.dart
@@ -0,0 +1,6 @@
+import 'package:test/test.dart';
+import 'embed_shelf_test.dart' as embed_shelf;
+
+main() {
+ group('embed_shelf', embed_shelf.main);
+}
diff --git a/packages/shelf/test/embed_shelf_test.dart b/packages/shelf/test/embed_shelf_test.dart
new file mode 100644
index 00000000..ae0f7a62
--- /dev/null
+++ b/packages/shelf/test/embed_shelf_test.dart
@@ -0,0 +1,115 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:angel_shelf/angel_shelf.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:charcode/charcode.dart';
+import 'package:http/http.dart' as http;
+import 'package:logging/logging.dart';
+import 'package:pretty_logging/pretty_logging.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+main() {
+ http.Client client;
+ HttpServer server;
+ String url;
+
+ String _path(String p) {
+ return Uri(
+ scheme: 'http',
+ host: server.address.address,
+ port: server.port,
+ path: p)
+ .toString();
+ }
+
+ setUp(() async {
+ client = http.Client();
+ var handler = shelf.Pipeline().addHandler((shelf.Request request) {
+ if (request.url.path == 'two') {
+ return shelf.Response(200, body: json.encode(2));
+ } else if (request.url.path == 'error') {
+ throw AngelHttpException.notFound();
+ } else if (request.url.path == 'status') {
+ return shelf.Response.notModified(headers: {'foo': 'bar'});
+ } else if (request.url.path == 'hijack') {
+ request.hijack((StreamChannel> channel) {
+ print('a');
+ var sink = channel.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();
+ print('b');
+ });
+ return null;
+ } else if (request.url.path == 'throw') {
+ return null;
+ } else {
+ return shelf.Response.ok('Request for "${request.url}"');
+ }
+ });
+
+ var logger = Logger.detached('angel_shelf')..onRecord.listen(prettyLog);
+ var app = Angel(logger: logger);
+ var httpDriver = AngelHttp(app);
+ app.get('/angel', (req, res) => 'Angel');
+ app.fallback(embedShelf(handler, throwOnNullResponse: true));
+
+ server = await httpDriver.startServer(InternetAddress.loopbackIPv4, 0);
+ });
+
+ tearDown(() async {
+ await client.close();
+ await server.close(force: true);
+ });
+
+ test('expose angel side', () async {
+ var response = await client.get(_path('/angel'));
+ expect(json.decode(response.body), equals('Angel'));
+ });
+
+ test('expose shelf side', () async {
+ var response = await client.get(_path('/foo'));
+ expect(response, hasStatus(200));
+ expect(response.body, equals('Request for "foo"'));
+ });
+
+ test('shelf can return arbitrary values', () async {
+ var response = await client.get(_path('/two'));
+ expect(response, isJson(2));
+ });
+
+ test('shelf can hijack', () async {
+ try {
+ var client = HttpClient();
+ var rq = await client.openUrl('GET', Uri.parse(_path('/hijack')));
+ var rs = await rq.close();
+ var body = await rs.cast>().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(_path('/status'));
+ expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar')));
+ });
+
+ test('shelf can throw error', () async {
+ var response = await client.get(_path('/error'));
+ expect(response, hasStatus(404));
+ });
+
+ test('throw on null', () async {
+ var response = await client.get(_path('/throw'));
+ expect(response, hasStatus(500));
+ });
+}