Add 'packages/shelf/' from commit '1923132532665f8a26d9a12fc9db2f2073611e45'
git-subtree-dir: packages/shelf git-subtree-mainline:a7842bddd8
git-subtree-split:1923132532
This commit is contained in:
commit
ee512d5ccf
22 changed files with 975 additions and 0 deletions
72
packages/shelf/.gitignore
vendored
Normal file
72
packages/shelf/.gitignore
vendored
Normal 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
|
8
packages/shelf/.idea/modules.xml
Normal file
8
packages/shelf/.idea/modules.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
16
packages/shelf/.idea/shelf.iml
Normal file
16
packages/shelf/.idea/shelf.iml
Normal 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>
|
6
packages/shelf/.idea/vcs.xml
Normal file
6
packages/shelf/.idea/vcs.xml
Normal 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>
|
1
packages/shelf/.travis.yml
Normal file
1
packages/shelf/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
9
packages/shelf/CHANGELOG.md
Normal file
9
packages/shelf/CHANGELOG.md
Normal 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
21
packages/shelf/LICENSE
Normal 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
105
packages/shelf/README.md
Normal 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(...));
|
||||
```
|
4
packages/shelf/analysis_options.yaml
Normal file
4
packages/shelf/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
53
packages/shelf/example/angel_in_shelf.dart
Normal file
53
packages/shelf/example/angel_in_shelf.dart
Normal 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');
|
||||
}
|
35
packages/shelf/example/main.dart
Normal file
35
packages/shelf/example/main.dart
Normal 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}');
|
||||
}
|
5
packages/shelf/lib/angel_shelf.dart
Normal file
5
packages/shelf/lib/angel_shelf.dart
Normal 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';
|
86
packages/shelf/lib/src/convert.dart
Normal file
86
packages/shelf/lib/src/convert.dart
Normal 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);
|
||||
}
|
35
packages/shelf/lib/src/embed_shelf.dart
Normal file
35
packages/shelf/lib/src/embed_shelf.dart
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
149
packages/shelf/lib/src/shelf_driver.dart
Normal file
149
packages/shelf/lib/src/shelf_driver.dart
Normal 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);
|
||||
}
|
||||
}
|
77
packages/shelf/lib/src/shelf_request.dart
Normal file
77
packages/shelf/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;
|
||||
}
|
140
packages/shelf/lib/src/shelf_response.dart
Normal file
140
packages/shelf/lib/src/shelf_response.dart
Normal 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;
|
||||
}
|
18
packages/shelf/pubspec.yaml
Normal file
18
packages/shelf/pubspec.yaml
Normal 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"
|
6
packages/shelf/test/all.dart
Normal file
6
packages/shelf/test/all.dart
Normal 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);
|
||||
}
|
115
packages/shelf/test/embed_shelf_test.dart
Normal file
115
packages/shelf/test/embed_shelf_test.dart
Normal 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));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue