Added supportShelf
This commit is contained in:
parent
9863205199
commit
afbd65e7f5
12 changed files with 429 additions and 62 deletions
44
.gitignore
vendored
44
.gitignore
vendored
|
@ -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
|
||||
|
|
6
.idea/runConfigurations/All_Tests__coverage_.xml
Normal file
6
.idea/runConfigurations/All_Tests__coverage_.xml
Normal 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>
|
88
README.md
88
README.md
|
@ -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`.
|
|
@ -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
65
lib/src/convert.dart
Normal 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
36
lib/src/embed_shelf.dart
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
40
lib/src/support_shelf.dart
Normal file
40
lib/src/support_shelf.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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
8
test/all.dart
Normal 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);
|
||||
}
|
|
@ -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}"');
|
||||
}
|
99
test/embed_shelf_test.dart
Normal file
99
test/embed_shelf_test.dart
Normal 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));
|
||||
});
|
||||
}
|
41
test/support_shelf_test.dart
Normal file
41
test/support_shelf_test.dart
Normal 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'));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue