Add 'packages/shelf/' from commit '1923132532665f8a26d9a12fc9db2f2073611e45'

git-subtree-dir: packages/shelf
git-subtree-mainline: a7842bddd8
git-subtree-split: 1923132532
This commit is contained in:
Tobe O 2020-02-15 18:22:23 -05:00
commit ee512d5ccf
22 changed files with 975 additions and 0 deletions

72
packages/shelf/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/shelf.iml" filepath="$PROJECT_DIR$/.idea/shelf.iml" />
</modules>
</component>
</project>

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

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in shelf" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1 @@
language: dart

View file

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

21
packages/shelf/LICENSE Normal file
View file

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

105
packages/shelf/README.md Normal file
View file

@ -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<ApiController>();
// 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<Foo>('from_shelf', Foo());
// `req.container` is also available.
var container = request.context['angel_shelf.container'] as Container;
container.make<Truck>().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(...));
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

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

View file

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

View file

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

View file

@ -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<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
{String handlerPath, Map<String, Object> context}) async {
var app = req.app;
var headers = <String, String>{};
req.headers.forEach((k, v) {
headers[k] = v.join(',');
});
headers.remove(HttpHeaders.transferEncodingHeader);
void Function(void Function(StreamChannel<List<int>>)) 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<List<int>> channel) hijack) async {
try {
print('a');
await res.detach();
print('b');
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);
} 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);
}

View file

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

View file

@ -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<shelf.Request, ShelfResponseContext,
Stream<shelf.Request>, ShelfRequestContext, ShelfResponseContext> {
final StreamController<shelf.Request> incomingRequests = StreamController();
final FutureOr<shelf.Response> Function() notFound;
AngelShelf(Angel app, {FutureOr<shelf.Response> 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<Stream<shelf.Request>> close() {
incomingRequests.close();
return super.close();
}
Future<Stream<shelf.Request>> Function(dynamic, int) get serverGenerator =>
(_, __) async => incomingRequests.stream;
static UnsupportedError _unsupported() => UnsupportedError(
'AngelShelf cannot mount a standalone server, or return a URI.');
Future<shelf.Response> 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<shelf.Response> 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<Cookie> 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<ShelfRequestContext> 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<ShelfResponseContext> createResponseContext(
shelf.Request request, ShelfResponseContext response,
[ShelfRequestContext correspondingRequest]) {
// Return the original response.
return Future.value(response..correspondingRequest = correspondingRequest);
}
@override
Stream<ShelfResponseContext> 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<int> data) {
response.add(data);
}
}

View 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;
}

View file

@ -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<ShelfResponseContext> {
final Angel app;
final StreamController<List<int>> _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<void> close() {
if (!_handlersAreDone) {
_isOpen = false;
}
_ctrl.close();
return super.close();
}
Iterable<String> __allowedEncodings;
Iterable<String> 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<List<int>> stream) {
if (!isOpen && isBuffered) throw ResponseContext.closed();
Stream<List<int>> output = stream;
if (encoders.isNotEmpty && correspondingRequest != null) {
if (_allowedEncodings != null) {
for (var encodingName in _allowedEncodings) {
Converter<List<int>, List<int>> 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<int> 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<int>, List<int>> 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<ShelfResponseContext> detach() {
_isDetached = true;
return this;
}
@override
bool get isBuffered => false;
@override
bool get isOpen => _isOpen && !_isDetached;
@override
void useBuffer() {}
@override
ShelfResponseContext get rawResponse => this;
}

View file

@ -0,0 +1,18 @@
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.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"

View file

@ -0,0 +1,6 @@
import 'package:test/test.dart';
import 'embed_shelf_test.dart' as embed_shelf;
main() {
group('embed_shelf', embed_shelf.main);
}

View file

@ -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<List<int>> 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<List<int>>().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));
});
}