Added supportShelf

This commit is contained in:
thosakwe 2017-06-12 19:53:08 -04:00
parent 9863205199
commit afbd65e7f5
12 changed files with 429 additions and 62 deletions

44
.gitignore vendored
View file

@ -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

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests (coverage)" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/all.dart" />
<method />
</configuration>
</component>

View file

@ -1,16 +1,29 @@
# shelf
[![version 1.0.0](https://img.shields.io/badge/pub-v1.0.0-brightgreen.svg)](https://pub.dartlang.org/packages/angel_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. 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`.

View file

@ -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';

65
lib/src/convert.dart Normal file
View file

@ -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<shelf.Request> convertRequest(RequestContext request,
{String handlerPath, Map<String, Object> context}) async {
var headers = <String, String>{};
request.headers.forEach((k, v) {
headers[k] = v.join(',');
});
headers.remove(HttpHeaders.TRANSFER_ENCODING);
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);
});
}).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();
}

36
lib/src/embed_shelf.dart Normal file
View file

@ -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<String, Object> 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;
}
};
}

View file

@ -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);
}
});
};
}

View file

@ -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 <thosakwe@gmail.com>
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

8
test/all.dart Normal file
View file

@ -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);
}

View file

@ -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}"');
}

View file

@ -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<List<int>> stream, StreamSink<List<int>> 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));
});
}

View file

@ -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'));
});
}