This commit is contained in:
Tobe O 2018-11-11 12:35:37 -05:00
parent 45a702b79f
commit d61aa82607
16 changed files with 161 additions and 282 deletions

View file

@ -0,0 +1 @@
2.0.0

Binary file not shown.

View file

@ -1,2 +1,5 @@
# 2.0.0
* Removed `supportShelf`.
# 1.2.0
* Upgraded for `>=1.1.0` compatibility.

View file

@ -3,7 +3,6 @@
[![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.
It also includes a plug-in that configures Angel to *natively* run `shelf` response handlers.
Use the code in this repo to embed existing shelf apps into
your Angel applications. This way, you can migrate legacy applications without
@ -12,15 +11,14 @@ 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](#usage)
- [embedShelf](#embedshelf)
- [Communicating with Angel](#communicating-with-angel-with-embedshelf)
# 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.
@ -32,27 +30,30 @@ import 'package:shelf/shelf.dart' as shelf;
import 'api/api.dart';
main() async {
final app = new Angel();
var app = new Angel();
var http = new AngelHttp(app);
// Angel routes on top
await app.configure(new ApiController());
await app.mountController<ApiController>();
// Re-route all other traffic to an
// existing application.
app.after.add(embedShelf(
app.fallback(embedShelf(
new shelf.Pipeline()
.addMiddleware(shelf.logRequests())
.addHandler(_echoRequest)
));
// Only on a specific route
app.get('/shelf', handler);
// Or, only on a specific route:
app.get('/shelf', wrappedShelfHandler);
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
await http.startServer(InternetAddress.loopbackIPV4, 3000);
print(http.uri);
}
```
### Communicating with Angel with embedShelf
You can communicate with Angel:
```dart
@ -61,55 +62,10 @@ handleRequest(shelf.Request 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;
req.container.registerNamedSingleton<Foo>('from_shelf', new Foo());
// `req.container` is also available.
var container = request.context['angel_shelf.container'] as Container;
container.make<Truck>().drive();
}
```
## 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`.

View file

@ -1,2 +1,3 @@
analyzer:
strong-mode: true
strong-mode:
implicit-casts: false

View file

@ -1,26 +0,0 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_shelf/angel_shelf.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
main() async {
var app = new Angel();
// `shelf` request handler
var shelfHandler = proxyHandler('https://www.dartlang.org');
// Use `embedShelf` to adapt a `shelf` handler for use within Angel.
var angelHandler = embedShelf(shelfHandler);
// A normal Angel route.
app.get('/angel', (req, ResponseContext res) {
res.write('Hooray for `package:angel_shelf`!');
res.end(); // End execution of handlers, so we don't proxy to dartlang.org when we don't need to.
});
// Proxy any other request through to dartlang.org
app.use(angelHandler);
var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 8080);
print('Proxying at http://${server.address.host}:${server.port}');
}

29
example/main.dart Normal file
View file

@ -0,0 +1,29 @@
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:shelf_static/shelf_static.dart';
main() async {
var app = new Angel();
var http = new 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.
});
// Proxy any other request through to the static file handler
app.fallback(wrappedHandler);
await http.startServer(InternetAddress.loopbackIPv4, 8080);
print('Proxying at ${http.uri}');
}

View file

@ -1,3 +1,2 @@
export 'src/convert.dart';
export 'src/embed_shelf.dart';
export 'src/support_shelf.dart';

View file

@ -1,50 +1,91 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.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 [request].
/// Creates a [shelf.Request] analogous to the input [req].
///
/// The new request's `context` will contain [request.properties] as `angel_shelf.properties`, as well as
/// The new 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* `storeOriginalBuffer` to `true`
/// If you want to read the request body, you *must* set `keepRawRequestBuffers` to `true`
/// on your application instance.
Future<shelf.Request> convertRequest(RequestContext request,
Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
{String handlerPath, Map<String, Object> context}) async {
var app = req.app;
var headers = <String, String>{};
request.headers.forEach((k, v) {
req.headers.forEach((k, v) {
headers[k] = v.join(',');
});
headers.remove(HttpHeaders.TRANSFER_ENCODING);
headers.remove(HttpHeaders.transferEncodingHeader);
void onHijack(
void hijack(Stream<List<int>> stream, StreamSink<List<int>> sink)) {
request.io.response.detachSocket(writeHeaders: false).then((socket) {
return request.lazyOriginalBuffer().then((buf) {
var ctrl = new StreamController<List<int>>()..add(buf ?? []);
socket.listen(ctrl.add, onError: ctrl.addError, onDone: ctrl.close);
hijack(socket, socket);
void Function(void Function(StreamChannel<List<int>>)) onHijack;
String protocolVersion;
Uri requestedUri;
if (req is HttpRequestContext && res is HttpResponseContext) {
protocolVersion = req.rawRequest.protocolVersion;
requestedUri = req.rawRequest.requestedUri;
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
new Future(() async {
var rs = res.detach();
var socket = await rs.detachSocket(writeHeaders: false);
var ctrl = new StreamChannelController<List<int>>();
var body = await req.parseRawRequestBuffer() ?? [];
ctrl.local.sink.add(body ?? []);
socket.listen(ctrl.local.sink.add,
onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close);
ctrl.local.stream.pipe(socket);
hijack(ctrl.foreign);
}).catchError((e, st) {
app.logger?.severe('An error occurred while hijacking a shelf request',
e, st as StackTrace);
});
};
} else if (req is Http2RequestContext && res is Http2ResponseContext) {
protocolVersion = '2.0';
requestedUri = req.uri;
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
new Future(() async {
var rs = await res.detach();
var ctrl = new StreamChannelController<List<int>>();
var body = await req.parseRawRequestBuffer() ?? [];
ctrl.local.sink.add(body ?? []);
ctrl.local.stream.listen(rs.sendData, onDone: rs.terminate);
hijack(ctrl.foreign);
}).catchError((e, st) {
stderr.writeln('An error occurred while hijacking a shelf request: $e');
stderr.writeln(st);
});
};
} else {
throw new UnsupportedError(
'`embedShelf` is only supported for HTTP and HTTP2 requests in Angel.');
}
return new shelf.Request(request.method, request.io.requestedUri,
protocolVersion: request.io.protocolVersion,
var url = req.uri;
if (p.isAbsolute(url.path)) {
url = url.replace(path: url.path.substring(1));
}
return new shelf.Request(req.method, requestedUri,
protocolVersion: 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})
url: url,
body: (await req.parseRawRequestBuffer()) ?? [],
context: {'angel_shelf.request': req}
..addAll({'angel_shelf.container': req.container})
..addAll(context ?? {}),
onHijack: onHijack);
}

View file

@ -15,8 +15,8 @@ RequestHandler embedShelf(shelf.Handler handler,
Map<String, Object> context,
bool throwOnNullResponse: false}) {
return (RequestContext req, ResponseContext res) async {
var shelfRequest =
await convertRequest(req, handlerPath: handlerPath, context: context);
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;
@ -25,10 +25,7 @@ RequestHandler embedShelf(shelf.Handler handler,
await mergeShelfResponse(result, res);
return false;
} on shelf.HijackException {
// On hijack...
res
..willCloseItself = true
..end();
// On hijack, do nothing, because the hijack handlers already call res.detach();
} catch (e) {
rethrow;
}

View file

@ -1,45 +0,0 @@
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.use((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;
mergeShelfResponse(val, res);
return ''; // Write nothing
};
return true;
});
/*
// 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);
}
});
*/
};
}

View file

@ -1,15 +1,16 @@
author: Tobe O <thosakwe@gmail.com>
description: Shelf interop with Angel.
description: Shelf interop with Angel. Use this to wrap existing server code.
homepage: https://github.com/angel-dart/shelf
name: angel_shelf
version: 1.2.0
version: 2.0.0
dependencies:
angel_framework: ">=1.1.0-alpha <2.0.0"
shelf: ^0.6.0
angel_framework: ^2.0.0-alpha
path: ^1.0.0
shelf: ^0.7.0
stream_channel: ^1.0.0
dev_dependencies:
angel_test: ^1.1.0
console: ^2.2.4
shelf_proxy: ^0.1.0
test: ^0.12.0
angel_test: ^2.0.0-alpha
shelf_static: ^0.2.8
test: ^1.0.0
environment:
sdk: ">=1.19.0"
sdk: ">=2.0.0-dev <3.0.0"

View file

@ -1,8 +1,6 @@
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);
}

View file

@ -1,15 +1,15 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_client/io.dart' as c;
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:logging/logging.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';
import 'pretty_logging.dart';
main() {
c.Angel client;
@ -19,16 +19,17 @@ main() {
setUp(() async {
var handler = new shelf.Pipeline().addHandler((shelf.Request request) {
if (request.url.path == 'two')
return 2;
return new shelf.Response(200, body: json.encode(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<List<int>> stream, StreamSink<List<int>> sink) {
sink.add(UTF8.encode('HTTP/1.1 200 OK\r\n'));
request.hijack((StreamChannel<List<int>> channel) {
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.add(utf8.encode(json.encode({'error': 'crime'})));
sink.close();
});
} else if (request.url.path == 'throw')
@ -37,14 +38,19 @@ main() {
return new shelf.Response.ok('Request for "${request.url}"');
});
var app = new Angel()..lazyParseBodies = true;
app.get('/angel', 'Angel');
app.use(embedShelf(handler, throwOnNullResponse: true));
app.logger = new Logger.detached('angel')..onRecord.listen(prettyLog);
var app = new Angel();
var http = new AngelHttp(app);
app.get('/angel', (req, res) => 'Angel');
app.fallback(embedShelf(handler, throwOnNullResponse: true));
app.logger = new Logger.detached('angel_shelf')
..onRecord.listen((rec) {
stdout.writeln(rec);
if (rec.error != null) stdout.writeln(rec.error);
if (rec.stackTrace != null) stdout.writeln(rec.stackTrace);
});
server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
client =
new c.Rest(url = 'http://${server.address.address}:${server.port}');
server = await http.startServer(InternetAddress.loopbackIPv4, 0);
client = new c.Rest(url = http.uri.toString());
});
tearDown(() async {
@ -54,7 +60,7 @@ main() {
test('expose angel side', () async {
var response = await client.get('/angel');
expect(JSON.decode(response.body), equals('Angel'));
expect(json.decode(response.body), equals('Angel'));
});
test('expose shelf side', () async {
@ -73,15 +79,15 @@ main() {
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();
var body = await rs.transform(utf8.decoder).join();
print('Response: $body');
expect(JSON.decode(body), {'error': 'crime'});
expect(json.decode(body), {'error': 'crime'});
} on HttpException catch (e, st) {
print('HTTP Exception: ' + e.message);
print(st);
rethrow;
}
});
}, skip: '');
test('shelf can set status code', () async {
var response = await client.get('/status');

View file

@ -1,33 +0,0 @@
import 'package:console/console.dart';
import 'package:logging/logging.dart';
import 'package:stack_trace/stack_trace.dart';
/// Prints the contents of a [LogRecord] with pretty colors.
void prettyLog(LogRecord record) {
var pen = new TextPen();
chooseLogColor(pen.reset(), record.level);
pen(record.toString());
if (record.error != null)
pen(record.error.toString());
if (record.stackTrace != null)
pen(new Chain.forTrace(record.stackTrace).terse.toString());
pen();
}
/// Chooses a color based on the logger [level].
void chooseLogColor(TextPen pen, Level level) {
if (level == Level.SHOUT)
pen.darkRed();
else if (level == Level.SEVERE)
pen.red();
else if (level == Level.WARNING)
pen.yellow();
else if (level == Level.INFO)
pen.magenta();
else if (level == Level.FINER)
pen.blue();
else if (level == Level.FINEST)
pen.darkBlue();
}

View file

@ -1,49 +0,0 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_shelf/angel_shelf.dart';
import 'package:angel_test/angel_test.dart';
import 'package:logging/logging.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:stack_trace/stack_trace.dart';
import 'package:test/test.dart';
import 'pretty_logging.dart';
main() {
TestClient client;
setUp(() async {
var app = new Angel()..lazyParseBodies = true;
await app.configure(supportShelf());
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');
});
app.logger = new Logger.detached('angel')..onRecord.listen(prettyLog);
client = await connectTo(app);
});
tearDown(() => client.close());
test('injected into request', () async {
var response = await client.get('/inject', headers: {'accept': 'application/json'});
print('Response: ${response.body}');
expect(response, isJson({'inject': true}));
});
test('can return shelf response', () {
return Chain.capture(() async {
var response = await client.get('/hello', headers: {'accept': 'application/json'});
print('Response: ${response.body}');
expect(response, hasBody('world'));
}, onError: (e, chain) {
print(e);
print(chain.terse);
expect(0, 1);
});
});
}