Init shelf driver
This commit is contained in:
parent
aacd91e267
commit
b115aae975
11 changed files with 160 additions and 84 deletions
|
@ -1,3 +1,7 @@
|
|||
# 2.1.0
|
||||
* `pedantic` lints.
|
||||
* Add the `AngelShelf` driver class, which allows you to embed Angel within shelf.
|
||||
|
||||
# 2.0.0
|
||||
* Removed `supportShelf`.
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ import 'package:shelf/shelf.dart' as shelf;
|
|||
import 'api/api.dart';
|
||||
|
||||
main() async {
|
||||
var app = new Angel();
|
||||
var http = new AngelHttp(app);
|
||||
var app = Angel();
|
||||
var http = AngelHttp(app);
|
||||
|
||||
// Angel routes on top
|
||||
await app.mountController<ApiController>();
|
||||
|
@ -39,7 +39,7 @@ main() async {
|
|||
// Re-route all other traffic to an
|
||||
// existing application.
|
||||
app.fallback(embedShelf(
|
||||
new shelf.Pipeline()
|
||||
shelf.Pipeline()
|
||||
.addMiddleware(shelf.logRequests())
|
||||
.addHandler(_echoRequest)
|
||||
));
|
||||
|
@ -62,7 +62,7 @@ handleRequest(shelf.Request request) {
|
|||
var req = request.context['angel_shelf.request'] as RequestContext;
|
||||
|
||||
// ... And then interact with it.
|
||||
req.container.registerNamedSingleton<Foo>('from_shelf', new Foo());
|
||||
req.container.registerNamedSingleton<Foo>('from_shelf', Foo());
|
||||
|
||||
// `req.container` is also available.
|
||||
var container = request.context['angel_shelf.container'] as Container;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
implicit-casts: false
|
||||
|
|
|
@ -2,11 +2,17 @@ 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 {
|
||||
var app = new Angel();
|
||||
var http = new AngelHttp(app);
|
||||
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('.',
|
||||
|
@ -21,9 +27,9 @@ main() async {
|
|||
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
|
||||
// Pass any other request through to the static file handler
|
||||
app.fallback(wrappedHandler);
|
||||
|
||||
await http.startServer(InternetAddress.loopbackIPv4, 8080);
|
||||
print('Proxying at ${http.uri}');
|
||||
print('Running at ${http.uri}');
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
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 [req].
|
||||
///
|
||||
/// The new request's `context` will contain [req.container] as `angel_shelf.container`, as well as
|
||||
/// 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`.
|
||||
|
@ -30,47 +28,26 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
|
|||
String protocolVersion;
|
||||
Uri requestedUri;
|
||||
|
||||
if (req is HttpRequestContext && res is HttpResponseContext) {
|
||||
protocolVersion = req.rawRequest.protocolVersion;
|
||||
requestedUri = req.rawRequest.requestedUri;
|
||||
protocolVersion = '1.1';
|
||||
requestedUri = Uri.parse('http://${req.hostname}');
|
||||
requestedUri = requestedUri.replace(path: req.uri.path);
|
||||
|
||||
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,
|
||||
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
|
||||
Future.sync(res.detach).then((_) {
|
||||
var ctrl = StreamChannelController<List<int>>();
|
||||
if (req.hasParsedBody) {
|
||||
req.body.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.');
|
||||
}
|
||||
} else {
|
||||
ctrl.local.sink.close();
|
||||
}
|
||||
ctrl.local.stream.pipe(res);
|
||||
hijack(ctrl.foreign);
|
||||
}).catchError((e, st) {
|
||||
app.logger?.severe('An error occurred while hijacking a shelf request', e,
|
||||
st as StackTrace);
|
||||
});
|
||||
};
|
||||
|
||||
var url = req.uri;
|
||||
|
||||
|
@ -78,12 +55,12 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
|
|||
url = url.replace(path: url.path.substring(1));
|
||||
}
|
||||
|
||||
return new shelf.Request(req.method, requestedUri,
|
||||
return shelf.Request(req.method, requestedUri,
|
||||
protocolVersion: protocolVersion,
|
||||
headers: headers,
|
||||
handlerPath: handlerPath,
|
||||
url: url,
|
||||
body: (await req.parseRawRequestBuffer()) ?? [],
|
||||
body: req.body,
|
||||
context: {'angel_shelf.request': req}
|
||||
..addAll({'angel_shelf.container': req.container})
|
||||
..addAll(context ?? {}),
|
||||
|
|
|
@ -13,19 +13,21 @@ import 'convert.dart';
|
|||
RequestHandler embedShelf(shelf.Handler handler,
|
||||
{String handlerPath,
|
||||
Map<String, Object> context,
|
||||
bool throwOnNullResponse: false}) {
|
||||
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 new AngelHttpException('Internal Server Error');
|
||||
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;
|
||||
}
|
||||
|
|
0
lib/src/shelf_driver.dart
Normal file
0
lib/src/shelf_driver.dart
Normal file
77
lib/src/shelf_request.dart
Normal file
77
lib/src/shelf_request.dart
Normal file
|
@ -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<Cookie> _cookies;
|
||||
|
||||
@override
|
||||
final MockHttpHeaders headers = MockHttpHeaders();
|
||||
|
||||
ShelfRequestContext(this.app, this.container, this.rawRequest, this.path) {
|
||||
rawRequest.headers.forEach(headers.add);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<int>> get body => rawRequest.read();
|
||||
|
||||
@override
|
||||
List<Cookie> 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;
|
||||
}
|
0
lib/src/shelf_response.dart
Normal file
0
lib/src/shelf_response.dart
Normal file
|
@ -2,7 +2,7 @@ author: Tobe O <thosakwe@gmail.com>
|
|||
description: Shelf interop with Angel. Use this to wrap existing server code.
|
||||
homepage: https://github.com/angel-dart/shelf
|
||||
name: angel_shelf
|
||||
version: 2.0.0
|
||||
version: 2.1.0
|
||||
dependencies:
|
||||
angel_framework: ^2.0.0-alpha
|
||||
path: ^1.0.0
|
||||
|
@ -10,6 +10,8 @@ dependencies:
|
|||
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:
|
||||
|
|
|
@ -7,6 +7,7 @@ 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:pretty_logging/pretty_logging.dart';
|
||||
import 'package:shelf/shelf.dart' as shelf;
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
@ -16,15 +17,24 @@ main() {
|
|||
HttpServer server;
|
||||
String url;
|
||||
|
||||
String _path(String p) {
|
||||
return Uri(
|
||||
scheme: 'http',
|
||||
host: server.address.address,
|
||||
port: server.port,
|
||||
path: p)
|
||||
.toString();
|
||||
}
|
||||
|
||||
setUp(() async {
|
||||
var handler = new shelf.Pipeline().addHandler((shelf.Request request) {
|
||||
if (request.url.path == 'two')
|
||||
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') {
|
||||
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<List<int>> channel) {
|
||||
var sink = channel.sink;
|
||||
sink.add(utf8.encode('HTTP/1.1 200 OK\r\n'));
|
||||
|
@ -32,25 +42,22 @@ main() {
|
|||
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}"');
|
||||
} else if (request.url.path == 'throw') {
|
||||
return null;
|
||||
} else {
|
||||
return shelf.Response.ok('Request for "${request.url}"');
|
||||
}
|
||||
});
|
||||
|
||||
var app = new Angel();
|
||||
var http = new AngelHttp(app);
|
||||
var logger = Logger.detached('angel_shelf')..onRecord.listen(prettyLog);
|
||||
var app = Angel(logger: logger);
|
||||
var http = 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 http.startServer(InternetAddress.loopbackIPv4, 0);
|
||||
client = new c.Rest(url = http.uri.toString());
|
||||
client = c.Rest(url = http.uri.toString());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
@ -59,24 +66,24 @@ main() {
|
|||
});
|
||||
|
||||
test('expose angel side', () async {
|
||||
var response = await client.get('/angel');
|
||||
var response = await client.get(_path('/angel'));
|
||||
expect(json.decode(response.body), equals('Angel'));
|
||||
});
|
||||
|
||||
test('expose shelf side', () async {
|
||||
var response = await client.get('/foo');
|
||||
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('/two');
|
||||
var response = await client.get(_path('/two'));
|
||||
expect(response, isJson(2));
|
||||
});
|
||||
|
||||
test('shelf can hijack', () async {
|
||||
try {
|
||||
var client = new HttpClient();
|
||||
var client = HttpClient();
|
||||
var rq = await client.openUrl('GET', Uri.parse('$url/hijack'));
|
||||
var rs = await rq.close();
|
||||
var body = await rs.cast<List<int>>().transform(utf8.decoder).join();
|
||||
|
@ -90,17 +97,17 @@ main() {
|
|||
}, skip: '');
|
||||
|
||||
test('shelf can set status code', () async {
|
||||
var response = await client.get('/status');
|
||||
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('/error');
|
||||
var response = await client.get(_path('/error'));
|
||||
expect(response, hasStatus(404));
|
||||
});
|
||||
|
||||
test('throw on null', () async {
|
||||
var response = await client.get('/throw');
|
||||
var response = await client.get(_path('/throw'));
|
||||
expect(response, hasStatus(500));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue