69
This commit is contained in:
parent
daafe2eaf6
commit
cc793f6736
16 changed files with 486 additions and 77 deletions
|
@ -1,6 +1,6 @@
|
|||
# angel_framework
|
||||
|
||||
[![pub 1.0.0-dev.68](https://img.shields.io/badge/pub-1.0.0--dev.68-red.svg)](https://pub.dartlang.org/packages/angel_framework)
|
||||
[![pub 1.0.0-dev.69](https://img.shields.io/badge/pub-1.0.0--dev.69-red.svg)](https://pub.dartlang.org/packages/angel_framework)
|
||||
[![build status](https://travis-ci.org/angel-dart/framework.svg)](https://travis-ci.org/angel-dart/framework)
|
||||
|
||||
Core libraries for the Angel Framework.
|
||||
|
|
5
example/common.dart
Normal file
5
example/common.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
Future<HttpServer> startShared(InternetAddress address, int port) => HttpServer
|
||||
.bind(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0, shared: true);
|
52
example/json.dart
Normal file
52
example/json.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() async {
|
||||
int x = 0;
|
||||
var c = new Completer();
|
||||
var exit = new ReceivePort();
|
||||
List<Isolate> isolates = [];
|
||||
|
||||
exit.listen((_) {
|
||||
if (++x >= 50) {
|
||||
c.complete();
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
var isolate = await Isolate.spawn(serverMain, null);
|
||||
isolates.add(isolate);
|
||||
print('Spawned isolate #${i + 1}...');
|
||||
|
||||
isolate.addOnExitListener(exit.sendPort);
|
||||
}
|
||||
|
||||
print('Angel listening at http://localhost:3000');
|
||||
await c.future;
|
||||
}
|
||||
|
||||
serverMain(_) async {
|
||||
var app = new Angel.custom(startShared); // Run a cluster
|
||||
|
||||
app.get('/', {
|
||||
"foo": "bar",
|
||||
"one": [2, "three"],
|
||||
"bar": {"baz": "quux"}
|
||||
});
|
||||
|
||||
// Performance tuning
|
||||
app
|
||||
..lazyParseBodies = true
|
||||
..injectSerializer(JSON.encode);
|
||||
|
||||
app.fatalErrorStream.listen((e) {
|
||||
print(e.error);
|
||||
print(e.stack);
|
||||
});
|
||||
|
||||
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
|
||||
}
|
114
lib/hooks.dart
114
lib/hooks.dart
|
@ -32,20 +32,8 @@ AngelConfigurer hookAllServices(callback(Service service)) {
|
|||
};
|
||||
}
|
||||
|
||||
/// Transforms `e.data` or `e.result` into JSON-friendly data, i.e. a Map.
|
||||
HookedServiceEventListener toJson() {
|
||||
return (HookedServiceEvent e) {
|
||||
normalize(obj) {
|
||||
if (obj != null && obj is! Map) return god.serializeObject(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (e.isBefore) {
|
||||
e.data = normalize(e.data);
|
||||
} else
|
||||
e.result = normalize(e.result);
|
||||
};
|
||||
}
|
||||
/// Transforms `e.data` or `e.result` into JSON-friendly data, i.e. a Map. Runs on Iterables as well.
|
||||
HookedServiceEventListener toJson() => transform(god.serializeObject);
|
||||
|
||||
/// Mutates `e.data` or `e.result` using the given [transformer].
|
||||
HookedServiceEventListener transform(transformer(obj)) {
|
||||
|
@ -61,7 +49,7 @@ HookedServiceEventListener transform(transformer(obj)) {
|
|||
return (HookedServiceEvent e) {
|
||||
if (e.isBefore) {
|
||||
e.data = normalize(e.data);
|
||||
} else
|
||||
} else if (e.isAfter)
|
||||
e.result = normalize(e.result);
|
||||
};
|
||||
}
|
||||
|
@ -134,8 +122,13 @@ HookedServiceEventListener remove(key, [remover(key, obj)]) {
|
|||
}
|
||||
}
|
||||
|
||||
if (e.params?.containsKey('provider') == true)
|
||||
await normalize(e.isBefore ? e.data : e.result);
|
||||
if (e.params?.containsKey('provider') == true) {
|
||||
if (e.isBefore) {
|
||||
e.data = await normalize(e.data);
|
||||
} else if (e.isAfter) {
|
||||
e.result = await normalize(e.result);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -168,3 +161,90 @@ HookedServiceEventListener disable([provider]) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes the current time to `e.data` or `e.result`.
|
||||
/// You can provide an [assign] function to set the property on your object, and skip reflection.
|
||||
///
|
||||
/// Default key: `createdAt`
|
||||
HookedServiceEventListener addCreatedAt({
|
||||
assign(obj, String now),
|
||||
String key,
|
||||
}) {
|
||||
var name = key?.isNotEmpty == true ? key : 'createdAt';
|
||||
|
||||
return (HookedServiceEvent e) async {
|
||||
_assign(obj, String now) {
|
||||
if (assign != null)
|
||||
return assign(obj, now);
|
||||
else if (obj is Map)
|
||||
obj.remove(name);
|
||||
else if (obj is Extensible)
|
||||
obj..properties.remove(name);
|
||||
else {
|
||||
try {
|
||||
reflect(obj).setField(new Symbol(name), now);
|
||||
} catch (e) {
|
||||
throw new ArgumentError("Cannot set key '$name' on $obj.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var now = new DateTime.now().toIso8601String();
|
||||
|
||||
normalize(obj) async {
|
||||
if (obj != null) {
|
||||
if (obj is Iterable) {
|
||||
obj.forEach(normalize);
|
||||
} else {
|
||||
await _assign(obj, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.params?.containsKey('provider') == true)
|
||||
await normalize(e.isBefore ? e.data : e.result);
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes the current time to `e.data` or `e.result`.
|
||||
/// You can provide an [assign] function to set the property on your object, and skip reflection.///
|
||||
/// Default key: `createdAt`
|
||||
HookedServiceEventListener addUpatedAt({
|
||||
assign(obj, String now),
|
||||
String key,
|
||||
}) {
|
||||
var name = key?.isNotEmpty == true ? key : 'updatedAt';
|
||||
|
||||
return (HookedServiceEvent e) async {
|
||||
_assign(obj, String now) {
|
||||
if (assign != null)
|
||||
return assign(obj, now);
|
||||
else if (obj is Map)
|
||||
obj.remove(name);
|
||||
else if (obj is Extensible)
|
||||
obj..properties.remove(name);
|
||||
else {
|
||||
try {
|
||||
reflect(obj).setField(new Symbol(name), now);
|
||||
} catch (e) {
|
||||
throw new ArgumentError("Cannot SET key '$name' ON $obj.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var now = new DateTime.now().toIso8601String();
|
||||
|
||||
normalize(obj) async {
|
||||
if (obj != null) {
|
||||
if (obj is Iterable) {
|
||||
obj.forEach(normalize);
|
||||
} else {
|
||||
await _assign(obj, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.params?.containsKey('provider') == true)
|
||||
await normalize(e.isBefore ? e.data : e.result);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,7 +13,12 @@ class AngelBase extends Routable {
|
|||
|
||||
Container _container = new Container();
|
||||
|
||||
/// When set to true, the original body bytes will be stored
|
||||
/// When set to true, the request body will not be parsed
|
||||
/// automatically. You can call `req.parse()` manually,
|
||||
/// or use `lazyBody()`.
|
||||
bool lazyParseBodies = false;
|
||||
|
||||
/// When set to `true`, the original body bytes will be stored
|
||||
/// on requests. `false` by default.
|
||||
bool storeOriginalBuffer = false;
|
||||
|
||||
|
|
|
@ -31,15 +31,26 @@ class InjectionRequest {
|
|||
class Controller {
|
||||
Angel _app;
|
||||
|
||||
/// The [Angel] application powering this controller.
|
||||
Angel get app => _app;
|
||||
|
||||
final bool debug;
|
||||
|
||||
/// If `true` (default), this class will inject itself as a singleton into the [app]'s container when bootstrapped.
|
||||
final bool injectSingleton;
|
||||
|
||||
/// Middleware to run before all handlers in this class.
|
||||
List middleware = [];
|
||||
|
||||
/// A mapping of route paths to routes, produced from the [Expose] annotations on this class.
|
||||
Map<String, Route> routeMappings = {};
|
||||
|
||||
Controller({this.debug: false});
|
||||
Controller({this.debug: false, this.injectSingleton: true});
|
||||
|
||||
Future call(Angel app) async {
|
||||
_app = app..container.singleton(this);
|
||||
_app = app;
|
||||
|
||||
if (injectSingleton != false) _app.container.singleton(this);
|
||||
|
||||
// Load global expose decl
|
||||
ClassMirror classMirror = reflectClass(this.runtimeType);
|
||||
|
|
|
@ -3,6 +3,7 @@ library angel_framework.http;
|
|||
import 'dart:async';
|
||||
import 'package:merge_map/merge_map.dart';
|
||||
import '../util.dart';
|
||||
import 'angel_http_exception.dart';
|
||||
import 'request_context.dart';
|
||||
import 'response_context.dart';
|
||||
import 'metadata.dart';
|
||||
|
@ -128,15 +129,18 @@ class HookedService extends Service {
|
|||
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
||||
|
||||
Middleware createMiddleware = getAnnotation(inner.create, Middleware);
|
||||
post(
|
||||
'/',
|
||||
(req, res) async => await this.create(
|
||||
req.body,
|
||||
|
||||
post('/', (req, res) async {
|
||||
var r = await this.create(
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])),
|
||||
]));
|
||||
res.statusCode = 201;
|
||||
return r;
|
||||
},
|
||||
middleware: []
|
||||
..addAll(handlers)
|
||||
..addAll(
|
||||
|
@ -162,7 +166,7 @@ class HookedService extends Service {
|
|||
'/:id',
|
||||
(req, res) async => await this.modify(
|
||||
toId(req.params['id']),
|
||||
req.body,
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
|
@ -178,7 +182,21 @@ class HookedService extends Service {
|
|||
'/:id',
|
||||
(req, res) async => await this.update(
|
||||
toId(req.params['id']),
|
||||
req.body,
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])),
|
||||
middleware: []
|
||||
..addAll(handlers)
|
||||
..addAll(
|
||||
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
||||
put(
|
||||
'/:id',
|
||||
(req, res) async => await this.update(
|
||||
toId(req.params['id']),
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
|
@ -190,6 +208,19 @@ class HookedService extends Service {
|
|||
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
||||
|
||||
Middleware removeMiddleware = getAnnotation(inner.remove, Middleware);
|
||||
delete(
|
||||
'/',
|
||||
(req, res) async => await this.remove(
|
||||
null,
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])),
|
||||
middleware: []
|
||||
..addAll(handlers)
|
||||
..addAll(
|
||||
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
||||
delete(
|
||||
'/:id',
|
||||
(req, res) async => await this.remove(
|
||||
|
@ -204,6 +235,10 @@ class HookedService extends Service {
|
|||
..addAll(
|
||||
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
||||
|
||||
// REST compliance
|
||||
put('/', () => throw new AngelHttpException.notFound());
|
||||
patch('/', () => throw new AngelHttpException.notFound());
|
||||
|
||||
addHooks();
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,8 @@ class MapService extends Service {
|
|||
throw new AngelHttpException.badRequest(
|
||||
message:
|
||||
'MapService does not support `modify` with ${data.runtimeType}.');
|
||||
if (!items.any(_matchesId(id))) return await create(data, params);
|
||||
|
||||
var item = await read(id);
|
||||
return item
|
||||
..addAll(data)
|
||||
|
@ -77,9 +79,7 @@ class MapService extends Service {
|
|||
throw new AngelHttpException.badRequest(
|
||||
message:
|
||||
'MapService does not support `update` with ${data.runtimeType}.');
|
||||
if (!items.any(_matchesId(id)))
|
||||
throw new AngelHttpException.notFound(
|
||||
message: 'No record found for ID $id');
|
||||
if (!items.any(_matchesId(id))) return await create(data, params);
|
||||
|
||||
var old = await read(id);
|
||||
|
||||
|
|
|
@ -45,17 +45,50 @@ class RequestContext extends Extensible {
|
|||
/// The original HTTP verb sent to the server.
|
||||
String get originalMethod => io.method;
|
||||
|
||||
StateError _unparsed(String type, String caps) => new StateError(
|
||||
'Cannot get the $type of an unparsed request. Use lazy${caps}() instead.');
|
||||
|
||||
/// All post data submitted to the server.
|
||||
Map get body => _body.body;
|
||||
///
|
||||
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
||||
/// then an error will be thrown.
|
||||
///
|
||||
/// **If you are writing a plug-in, use [lazyBody] instead.**
|
||||
Map get body {
|
||||
if (_body == null)
|
||||
throw _unparsed('body', 'Body');
|
||||
else
|
||||
return _body.body;
|
||||
}
|
||||
|
||||
/// The content type of an incoming request.
|
||||
ContentType get contentType => _contentType;
|
||||
|
||||
/// Any and all files sent to the server with this request.
|
||||
List<FileUploadInfo> get files => _body.files;
|
||||
///
|
||||
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
||||
/// then an error will be thrown.
|
||||
///
|
||||
/// **If you are writing a plug-in, use [lazyFiles] instead.**
|
||||
List<FileUploadInfo> get files {
|
||||
if (_body == null)
|
||||
throw _unparsed('query', 'Files');
|
||||
else
|
||||
return _body.files;
|
||||
}
|
||||
|
||||
/// The original body bytes sent with this request. May be empty.
|
||||
List<int> get originalBuffer => _body.originalBuffer ?? [];
|
||||
///
|
||||
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
||||
/// then an error will be thrown.
|
||||
///
|
||||
/// **If you are writing a plug-in, use [lazyOriginalBuffer] instead.**
|
||||
List<int> get originalBuffer {
|
||||
if (_body == null)
|
||||
throw _unparsed('original buffer', 'OriginalBuffer');
|
||||
else
|
||||
return _body.originalBuffer ?? [];
|
||||
}
|
||||
|
||||
/// The URL parameters extracted from the request URI.
|
||||
Map params = {};
|
||||
|
@ -64,7 +97,17 @@ class RequestContext extends Extensible {
|
|||
String get path => _path;
|
||||
|
||||
/// The parsed request query string.
|
||||
Map get query => _body.query;
|
||||
///
|
||||
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
||||
/// then [uri].query will be returned.
|
||||
///
|
||||
/// **If you are writing a plug-in, consider using [lazyQuery] instead.**
|
||||
Map get query {
|
||||
if (_body == null)
|
||||
return uri.queryParameters;
|
||||
else
|
||||
return _body.query;
|
||||
}
|
||||
|
||||
/// The remote address requesting this resource.
|
||||
InternetAddress get remoteAddress => io.connectionInfo.remoteAddress;
|
||||
|
@ -106,6 +149,7 @@ class RequestContext extends Extensible {
|
|||
.replaceAll(new RegExp(r'/+$'), '');
|
||||
ctx._io = request;
|
||||
|
||||
if (app.lazyParseBodies != true)
|
||||
ctx._body = (await parseBody(request,
|
||||
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
|
||||
{};
|
||||
|
@ -133,7 +177,39 @@ class RequestContext extends Extensible {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Shorthand to add to [injections].
|
||||
void inject(type, value) {
|
||||
injections[type] = value;
|
||||
}
|
||||
|
||||
/// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the body.
|
||||
Future<Map> lazyBody() => parse().then((b) => b.body);
|
||||
|
||||
/// Retrieves the request files if it has already been parsed, or lazy-parses it before returning the files.
|
||||
Future<List<FileUploadInfo>> lazyFiles() => parse().then((b) => b.files);
|
||||
|
||||
/// Retrieves the request files if it has already been parsed, or lazy-parses it before returning the files.
|
||||
///
|
||||
/// This will return an empty `List` if you have not enabled `storeOriginalBuffer` on your [app] instance.
|
||||
Future<List<int>> lazyOriginalBuffer() =>
|
||||
parse().then((b) => b.originalBuffer);
|
||||
|
||||
/// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the query.
|
||||
///
|
||||
/// If [forceParse] is not `true`, then [uri].query will be returned, and no parsing will be performed.
|
||||
Future<Map<String, dynamic>> lazyQuery({bool forceParse: false}) {
|
||||
if (_body == null && forceParse != true)
|
||||
return new Future.value(uri.query);
|
||||
else
|
||||
return parse().then((b) => b.query);
|
||||
}
|
||||
|
||||
/// Manually parses the request body, if it has not already been parsed.
|
||||
Future<BodyParseResult> parse() async {
|
||||
if (_body != null)
|
||||
return _body;
|
||||
else
|
||||
return _body = await parseBody(io,
|
||||
storeOriginalBuffer: app.storeOriginalBuffer == true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ typedef String ResponseSerializer(data);
|
|||
|
||||
/// A convenience wrapper around an outgoing HTTP request.
|
||||
class ResponseContext extends Extensible {
|
||||
final _LockableBytesBuilder _buffer = new _LockableBytesBuilder();
|
||||
final Map<String, String> _headers = {HttpHeaders.SERVER: 'angel'};
|
||||
bool _isOpen = true;
|
||||
|
||||
/// The [Angel] instance that is sending a response.
|
||||
|
@ -32,11 +34,27 @@ class ResponseContext extends Extensible {
|
|||
final List<Cookie> cookies = [];
|
||||
|
||||
/// Headers that will be sent to the user.
|
||||
final Map<String, String> headers = {HttpHeaders.SERVER: 'angel'};
|
||||
///
|
||||
/// If the response is closed, then this getter will return an immutable `Map`.
|
||||
Map<String, String> get headers {
|
||||
if (!_isOpen)
|
||||
return new Map<String, String>.unmodifiable(_headers);
|
||||
else
|
||||
return _headers;
|
||||
}
|
||||
|
||||
/// Serializes response data into a String.
|
||||
///
|
||||
/// The default is conversion into JSON.
|
||||
/// The default is conversion into JSON via `package:json_god`.
|
||||
///
|
||||
/// If you are 100% sure that your response handlers will only
|
||||
/// be JSON-encodable objects (i.e. primitives, `List`s and `Map`s),
|
||||
/// then consider setting [serializer] to `JSON.encode`.
|
||||
///
|
||||
/// To set it globally for the whole [app], use the following helper:
|
||||
/// ```dart
|
||||
/// app.injectSerializer(JSON.encode);
|
||||
/// ```
|
||||
ResponseSerializer serializer = god.serialize;
|
||||
|
||||
/// This response's status code.
|
||||
|
@ -46,7 +64,7 @@ class ResponseContext extends Extensible {
|
|||
bool get isOpen => _isOpen;
|
||||
|
||||
/// A set of UTF-8 encoded bytes that will be written to the response.
|
||||
final BytesBuilder buffer = new BytesBuilder();
|
||||
BytesBuilder get buffer => _buffer;
|
||||
|
||||
/// Sets the status code to be sent with this response.
|
||||
@Deprecated('Please use `statusCode=` instead.')
|
||||
|
@ -91,8 +109,12 @@ class ResponseContext extends Extensible {
|
|||
/// If `true`, all response finalizers will be skipped.
|
||||
bool willCloseItself = false;
|
||||
|
||||
StateError _closed() => new StateError('Cannot modify a closed response.');
|
||||
|
||||
/// Sends a download as a response.
|
||||
download(File file, {String filename}) async {
|
||||
if (!_isOpen) throw _closed();
|
||||
|
||||
headers["Content-Disposition"] =
|
||||
'attachment; filename="${filename ?? file.path}"';
|
||||
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
||||
|
@ -103,6 +125,7 @@ class ResponseContext extends Extensible {
|
|||
|
||||
/// Prevents more data from being written to the response.
|
||||
void end() {
|
||||
_buffer._lock();
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
|
@ -119,14 +142,24 @@ class ResponseContext extends Extensible {
|
|||
void json(value) => serialize(value, contentType: ContentType.JSON);
|
||||
|
||||
/// Returns a JSONP response.
|
||||
void jsonp(value, {String callbackName: "callback"}) {
|
||||
write("$callbackName(${god.serialize(value)})");
|
||||
headers[HttpHeaders.CONTENT_TYPE] = "application/javascript";
|
||||
void jsonp(value, {String callbackName: "callback", contentType}) {
|
||||
if (!_isOpen) throw _closed();
|
||||
write("$callbackName(${serializer(value)})");
|
||||
|
||||
if (contentType != null) {
|
||||
if (contentType is ContentType)
|
||||
this.contentType = contentType;
|
||||
else
|
||||
headers[HttpHeaders.CONTENT_TYPE] = contentType.toString();
|
||||
} else
|
||||
headers[HttpHeaders.CONTENT_TYPE] = 'application/javascript';
|
||||
|
||||
end();
|
||||
}
|
||||
|
||||
/// Renders a view to the response stream, and closes the response.
|
||||
Future render(String view, [Map data]) async {
|
||||
if (!_isOpen) throw _closed();
|
||||
write(await app.viewGenerator(view, data));
|
||||
headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString();
|
||||
end();
|
||||
|
@ -140,6 +173,7 @@ class ResponseContext extends Extensible {
|
|||
///
|
||||
/// See [Router]#navigate for more. :)
|
||||
void redirect(url, {bool absolute: true, int code: 302}) {
|
||||
if (!_isOpen) throw _closed();
|
||||
headers[HttpHeaders.LOCATION] =
|
||||
url is String ? url : app.navigate(url, absolute: absolute);
|
||||
statusCode = code ?? 302;
|
||||
|
@ -165,6 +199,7 @@ class ResponseContext extends Extensible {
|
|||
|
||||
/// Redirects to the given named [Route].
|
||||
void redirectTo(String name, [Map params, int code]) {
|
||||
if (!_isOpen) throw _closed();
|
||||
Route _findRoute(Router r) {
|
||||
for (Route route in r.routes) {
|
||||
if (route is SymlinkRoute) {
|
||||
|
@ -189,6 +224,7 @@ class ResponseContext extends Extensible {
|
|||
|
||||
/// Redirects to the given [Controller] action.
|
||||
void redirectToAction(String action, [Map params, int code]) {
|
||||
if (!_isOpen) throw _closed();
|
||||
// UserController@show
|
||||
List<String> split = action.split("@");
|
||||
|
||||
|
@ -218,7 +254,7 @@ class ResponseContext extends Extensible {
|
|||
/// Copies a file's contents into the response buffer.
|
||||
Future sendFile(File file,
|
||||
{int chunkSize, int sleepMs: 0, bool resumable: true}) async {
|
||||
if (!isOpen) return;
|
||||
if (!_isOpen) throw _closed();
|
||||
|
||||
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
||||
buffer.add(await file.readAsBytes());
|
||||
|
@ -229,6 +265,7 @@ class ResponseContext extends Extensible {
|
|||
///
|
||||
/// [contentType] can be either a [String], or a [ContentType].
|
||||
void serialize(value, {contentType}) {
|
||||
if (!_isOpen) throw _closed();
|
||||
var text = serializer(value);
|
||||
write(text);
|
||||
|
||||
|
@ -247,7 +284,7 @@ class ResponseContext extends Extensible {
|
|||
int sleepMs: 0,
|
||||
bool resumable: true,
|
||||
Codec<List<int>, List<int>> codec}) async {
|
||||
if (!isOpen) return;
|
||||
if (!_isOpen) throw _closed();
|
||||
|
||||
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
||||
end();
|
||||
|
@ -261,7 +298,9 @@ class ResponseContext extends Extensible {
|
|||
|
||||
/// Writes data to the response.
|
||||
void write(value, {Encoding encoding: UTF8}) {
|
||||
if (isOpen) {
|
||||
if (!_isOpen)
|
||||
throw _closed();
|
||||
else {
|
||||
if (value is List<int>)
|
||||
buffer.add(value);
|
||||
else
|
||||
|
@ -269,3 +308,73 @@ class ResponseContext extends Extensible {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LockableBytesBuilder extends BytesBuilder {
|
||||
factory _LockableBytesBuilder() => new _LockableBytesBuilderImpl();
|
||||
void _lock();
|
||||
}
|
||||
|
||||
class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
|
||||
bool _closed = false;
|
||||
final List<int> _data = [];
|
||||
|
||||
StateError _deny() =>
|
||||
new StateError('Cannot modified a closed response\'s buffer.');
|
||||
|
||||
@override
|
||||
void _lock() {
|
||||
_closed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void add(List<int> bytes) {
|
||||
if (_closed)
|
||||
throw _deny();
|
||||
else {
|
||||
_data.addAll(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addByte(int byte) {
|
||||
if (_closed)
|
||||
throw _deny();
|
||||
else {
|
||||
_data.add(byte);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
if (_closed)
|
||||
throw _deny();
|
||||
else {
|
||||
_data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isEmpty => _data.isEmpty;
|
||||
|
||||
@override
|
||||
bool get isNotEmpty => _data.isNotEmpty;
|
||||
|
||||
@override
|
||||
int get length => _data.length;
|
||||
|
||||
@override
|
||||
List<int> takeBytes() {
|
||||
if (_closed)
|
||||
return toBytes();
|
||||
else {
|
||||
var r = new List<int>.from(_data);
|
||||
clear();
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<int> toBytes() {
|
||||
return _data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,19 +19,12 @@ typedef Future<bool> RequestMiddleware(RequestContext req, ResponseContext res);
|
|||
/// A function that receives an incoming [RequestContext] and responds to it.
|
||||
typedef Future RequestHandler(RequestContext req, ResponseContext res);
|
||||
|
||||
/// Sequentially runs a list of [handlers] of middleware, and breaks if any does not
|
||||
/// Sequentially runs a list of [handlers] of middleware, and returns early if any does not
|
||||
/// return `true`. Works well with [Router].chain.
|
||||
RequestMiddleware waterfall(List handlers) {
|
||||
return (RequestContext req, res) async {
|
||||
for (var handler in handlers) {
|
||||
if (handler is! RequestMiddleware && handler is! RequestHandler)
|
||||
throw new ArgumentError(
|
||||
'`waterfall` only accepts middleware and handlers. $handler is not a valid option.');
|
||||
}
|
||||
|
||||
return (req, res) async {
|
||||
for (var handler in handlers) {
|
||||
var result = await handler(req, res);
|
||||
|
||||
var result = await req.app.executeHandler(handler, req, res);
|
||||
if (result != true) return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,9 @@ class Angel extends AngelBase {
|
|||
/// `'production'`.
|
||||
bool get isProduction => Platform.environment['ANGEL_ENV'] == 'production';
|
||||
|
||||
/// The function used to bind this instance to an HTTP server.
|
||||
ServerGenerator get serverGenerator => _serverGenerator;
|
||||
|
||||
/// Fired whenever a controller is added to this instance.
|
||||
///
|
||||
/// **NOTE**: This is a broadcast stream.
|
||||
|
@ -78,7 +81,8 @@ class Angel extends AngelBase {
|
|||
|
||||
/// Always run before responses are sent.
|
||||
///
|
||||
/// These will only not run if an [AngelFatalError] occurs.
|
||||
/// These will only not run if an [AngelFatalError] occurs,
|
||||
/// or if a response's `willCloseItself` is set to `true`.
|
||||
final List<RequestHandler> responseFinalizers = [];
|
||||
|
||||
/// The handler currently configured to run on [AngelHttpException]s.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
library angel_framework.http.service;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:angel_framework/src/http/response_context.dart';
|
||||
import 'package:merge_map/merge_map.dart';
|
||||
import '../util.dart';
|
||||
import 'angel_base.dart';
|
||||
|
@ -100,15 +101,17 @@ class Service extends Routable {
|
|||
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
||||
|
||||
Middleware createMiddleware = getAnnotation(this.create, Middleware);
|
||||
post(
|
||||
'/',
|
||||
(req, res) async => await this.create(
|
||||
req.body,
|
||||
post('/', (req, ResponseContext res) async {
|
||||
var r = await this.create(
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])),
|
||||
]));
|
||||
res.statusCode = 201;
|
||||
return r;
|
||||
},
|
||||
middleware: []
|
||||
..addAll(handlers)
|
||||
..addAll(
|
||||
|
@ -134,7 +137,7 @@ class Service extends Routable {
|
|||
'/:id',
|
||||
(req, res) async => await this.modify(
|
||||
toId(req.params['id']),
|
||||
req.body,
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
|
@ -150,7 +153,21 @@ class Service extends Routable {
|
|||
'/:id',
|
||||
(req, res) async => await this.update(
|
||||
toId(req.params['id']),
|
||||
req.body,
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])),
|
||||
middleware: []
|
||||
..addAll(handlers)
|
||||
..addAll(
|
||||
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
||||
put(
|
||||
'/:id',
|
||||
(req, res) async => await this.update(
|
||||
toId(req.params['id']),
|
||||
await req.lazyBody(),
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
|
@ -162,6 +179,19 @@ class Service extends Routable {
|
|||
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
||||
|
||||
Middleware removeMiddleware = getAnnotation(this.remove, Middleware);
|
||||
delete(
|
||||
'/',
|
||||
(req, res) async => await this.remove(
|
||||
null,
|
||||
mergeMap([
|
||||
{'query': req.query},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])),
|
||||
middleware: []
|
||||
..addAll(handlers)
|
||||
..addAll(
|
||||
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
||||
delete(
|
||||
'/:id',
|
||||
(req, res) async => await this.remove(
|
||||
|
@ -175,6 +205,10 @@ class Service extends Routable {
|
|||
..addAll(handlers)
|
||||
..addAll(
|
||||
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
||||
|
||||
// REST compliance
|
||||
put('/', () => throw new AngelHttpException.notFound());
|
||||
patch('/', () => throw new AngelHttpException.notFound());
|
||||
}
|
||||
|
||||
/// Invoked when this service is wrapped within a [HookedService].
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: angel_framework
|
||||
version: 1.0.0-dev.68
|
||||
version: 1.0.0-dev.69
|
||||
description: Core libraries for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_framework
|
||||
|
|
|
@ -59,7 +59,7 @@ main() {
|
|||
middleware: ['interceptor']);
|
||||
app.get('/hello', 'world');
|
||||
app.get('/name/:first/last/:last', (req, res) => req.params);
|
||||
app.post('/lambda', (req, res) => req.body);
|
||||
app.post('/lambda', (RequestContext req, res) => req.lazyBody());
|
||||
app.use('/todos/:id', todos);
|
||||
app
|
||||
.get('/greet/:name',
|
||||
|
|
|
@ -56,6 +56,7 @@ main() {
|
|||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
var response =
|
||||
await client.post("$url/todos", headers: headers, body: postData);
|
||||
expect(response.statusCode, 201);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('Hello, world!'));
|
||||
|
@ -65,6 +66,7 @@ main() {
|
|||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post("$url/todos", headers: headers, body: postData);
|
||||
var response = await client.get("$url/todos/0");
|
||||
expect(response.statusCode, 200);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('Hello, world!'));
|
||||
|
@ -76,6 +78,7 @@ main() {
|
|||
postData = god.serialize({'text': 'modified'});
|
||||
var response =
|
||||
await client.patch("$url/todos/0", headers: headers, body: postData);
|
||||
expect(response.statusCode, 200);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('modified'));
|
||||
|
@ -87,6 +90,7 @@ main() {
|
|||
postData = god.serialize({'over': 'write'});
|
||||
var response =
|
||||
await client.post("$url/todos/0", headers: headers, body: postData);
|
||||
expect(response.statusCode, 200);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals(null));
|
||||
|
@ -97,6 +101,7 @@ main() {
|
|||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post("$url/todos", headers: headers, body: postData);
|
||||
var response = await client.delete("$url/todos/0");
|
||||
expect(response.statusCode, 200);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('Hello, world!'));
|
||||
|
|
Loading…
Reference in a new issue