This commit is contained in:
thosakwe 2017-03-28 19:29:22 -04:00
parent daafe2eaf6
commit cc793f6736
16 changed files with 486 additions and 77 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
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();
}
@ -522,7 +557,7 @@ class HookedServiceEvent {
}
/// Resolves a service from the application.
///
///
/// Shorthand for `e.service.app.service(...)`.
Service getService(Pattern path) => service.app.service(path);

View file

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

View file

@ -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,9 +149,10 @@ class RequestContext extends Extensible {
.replaceAll(new RegExp(r'/+$'), '');
ctx._io = request;
ctx._body = (await parseBody(request,
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
{};
if (app.lazyParseBodies != true)
ctx._body = (await parseBody(request,
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
{};
return ctx;
}
@ -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);
}
}

View file

@ -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,9 +265,10 @@ 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);
if (contentType is String)
headers[HttpHeaders.CONTENT_TYPE] = contentType;
else if (contentType is ContentType) this.contentType = contentType;
@ -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;
}
}

View file

@ -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) {
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 {
return (RequestContext 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;
}

View file

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

View file

@ -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,
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
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].

View file

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

View file

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

View file

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