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
|
# 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)
|
[![build status](https://travis-ci.org/angel-dart/framework.svg)](https://travis-ci.org/angel-dart/framework)
|
||||||
|
|
||||||
Core libraries for the Angel 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.
|
/// Transforms `e.data` or `e.result` into JSON-friendly data, i.e. a Map. Runs on Iterables as well.
|
||||||
HookedServiceEventListener toJson() {
|
HookedServiceEventListener toJson() => transform(god.serializeObject);
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutates `e.data` or `e.result` using the given [transformer].
|
/// Mutates `e.data` or `e.result` using the given [transformer].
|
||||||
HookedServiceEventListener transform(transformer(obj)) {
|
HookedServiceEventListener transform(transformer(obj)) {
|
||||||
|
@ -61,7 +49,7 @@ HookedServiceEventListener transform(transformer(obj)) {
|
||||||
return (HookedServiceEvent e) {
|
return (HookedServiceEvent e) {
|
||||||
if (e.isBefore) {
|
if (e.isBefore) {
|
||||||
e.data = normalize(e.data);
|
e.data = normalize(e.data);
|
||||||
} else
|
} else if (e.isAfter)
|
||||||
e.result = normalize(e.result);
|
e.result = normalize(e.result);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -134,8 +122,13 @@ HookedServiceEventListener remove(key, [remover(key, obj)]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.params?.containsKey('provider') == true)
|
if (e.params?.containsKey('provider') == true) {
|
||||||
await normalize(e.isBefore ? e.data : e.result);
|
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();
|
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.
|
/// on requests. `false` by default.
|
||||||
bool storeOriginalBuffer = false;
|
bool storeOriginalBuffer = false;
|
||||||
|
|
||||||
|
|
|
@ -31,15 +31,26 @@ class InjectionRequest {
|
||||||
class Controller {
|
class Controller {
|
||||||
Angel _app;
|
Angel _app;
|
||||||
|
|
||||||
|
/// The [Angel] application powering this controller.
|
||||||
Angel get app => _app;
|
Angel get app => _app;
|
||||||
|
|
||||||
final bool debug;
|
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 = [];
|
List middleware = [];
|
||||||
|
|
||||||
|
/// A mapping of route paths to routes, produced from the [Expose] annotations on this class.
|
||||||
Map<String, Route> routeMappings = {};
|
Map<String, Route> routeMappings = {};
|
||||||
|
|
||||||
Controller({this.debug: false});
|
Controller({this.debug: false, this.injectSingleton: true});
|
||||||
|
|
||||||
Future call(Angel app) async {
|
Future call(Angel app) async {
|
||||||
_app = app..container.singleton(this);
|
_app = app;
|
||||||
|
|
||||||
|
if (injectSingleton != false) _app.container.singleton(this);
|
||||||
|
|
||||||
// Load global expose decl
|
// Load global expose decl
|
||||||
ClassMirror classMirror = reflectClass(this.runtimeType);
|
ClassMirror classMirror = reflectClass(this.runtimeType);
|
||||||
|
|
|
@ -3,6 +3,7 @@ library angel_framework.http;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:merge_map/merge_map.dart';
|
import 'package:merge_map/merge_map.dart';
|
||||||
import '../util.dart';
|
import '../util.dart';
|
||||||
|
import 'angel_http_exception.dart';
|
||||||
import 'request_context.dart';
|
import 'request_context.dart';
|
||||||
import 'response_context.dart';
|
import 'response_context.dart';
|
||||||
import 'metadata.dart';
|
import 'metadata.dart';
|
||||||
|
@ -128,15 +129,18 @@ class HookedService extends Service {
|
||||||
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
||||||
|
|
||||||
Middleware createMiddleware = getAnnotation(inner.create, Middleware);
|
Middleware createMiddleware = getAnnotation(inner.create, Middleware);
|
||||||
post(
|
|
||||||
'/',
|
post('/', (req, res) async {
|
||||||
(req, res) async => await this.create(
|
var r = await this.create(
|
||||||
req.body,
|
await req.lazyBody(),
|
||||||
mergeMap([
|
mergeMap([
|
||||||
{'query': req.query},
|
{'query': req.query},
|
||||||
restProvider,
|
restProvider,
|
||||||
req.serviceParams
|
req.serviceParams
|
||||||
])),
|
]));
|
||||||
|
res.statusCode = 201;
|
||||||
|
return r;
|
||||||
|
},
|
||||||
middleware: []
|
middleware: []
|
||||||
..addAll(handlers)
|
..addAll(handlers)
|
||||||
..addAll(
|
..addAll(
|
||||||
|
@ -162,7 +166,7 @@ class HookedService extends Service {
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res) async => await this.modify(
|
(req, res) async => await this.modify(
|
||||||
toId(req.params['id']),
|
toId(req.params['id']),
|
||||||
req.body,
|
await req.lazyBody(),
|
||||||
mergeMap([
|
mergeMap([
|
||||||
{'query': req.query},
|
{'query': req.query},
|
||||||
restProvider,
|
restProvider,
|
||||||
|
@ -178,7 +182,21 @@ class HookedService extends Service {
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res) async => await this.update(
|
(req, res) async => await this.update(
|
||||||
toId(req.params['id']),
|
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([
|
mergeMap([
|
||||||
{'query': req.query},
|
{'query': req.query},
|
||||||
restProvider,
|
restProvider,
|
||||||
|
@ -190,6 +208,19 @@ class HookedService extends Service {
|
||||||
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
||||||
|
|
||||||
Middleware removeMiddleware = getAnnotation(inner.remove, Middleware);
|
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(
|
delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res) async => await this.remove(
|
(req, res) async => await this.remove(
|
||||||
|
@ -204,6 +235,10 @@ class HookedService extends Service {
|
||||||
..addAll(
|
..addAll(
|
||||||
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
||||||
|
|
||||||
|
// REST compliance
|
||||||
|
put('/', () => throw new AngelHttpException.notFound());
|
||||||
|
patch('/', () => throw new AngelHttpException.notFound());
|
||||||
|
|
||||||
addHooks();
|
addHooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,8 @@ class MapService extends Service {
|
||||||
throw new AngelHttpException.badRequest(
|
throw new AngelHttpException.badRequest(
|
||||||
message:
|
message:
|
||||||
'MapService does not support `modify` with ${data.runtimeType}.');
|
'MapService does not support `modify` with ${data.runtimeType}.');
|
||||||
|
if (!items.any(_matchesId(id))) return await create(data, params);
|
||||||
|
|
||||||
var item = await read(id);
|
var item = await read(id);
|
||||||
return item
|
return item
|
||||||
..addAll(data)
|
..addAll(data)
|
||||||
|
@ -77,9 +79,7 @@ class MapService extends Service {
|
||||||
throw new AngelHttpException.badRequest(
|
throw new AngelHttpException.badRequest(
|
||||||
message:
|
message:
|
||||||
'MapService does not support `update` with ${data.runtimeType}.');
|
'MapService does not support `update` with ${data.runtimeType}.');
|
||||||
if (!items.any(_matchesId(id)))
|
if (!items.any(_matchesId(id))) return await create(data, params);
|
||||||
throw new AngelHttpException.notFound(
|
|
||||||
message: 'No record found for ID $id');
|
|
||||||
|
|
||||||
var old = await read(id);
|
var old = await read(id);
|
||||||
|
|
||||||
|
|
|
@ -45,17 +45,50 @@ class RequestContext extends Extensible {
|
||||||
/// The original HTTP verb sent to the server.
|
/// The original HTTP verb sent to the server.
|
||||||
String get originalMethod => io.method;
|
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.
|
/// 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.
|
/// The content type of an incoming request.
|
||||||
ContentType get contentType => _contentType;
|
ContentType get contentType => _contentType;
|
||||||
|
|
||||||
/// Any and all files sent to the server with this request.
|
/// 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.
|
/// 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.
|
/// The URL parameters extracted from the request URI.
|
||||||
Map params = {};
|
Map params = {};
|
||||||
|
@ -64,7 +97,17 @@ class RequestContext extends Extensible {
|
||||||
String get path => _path;
|
String get path => _path;
|
||||||
|
|
||||||
/// The parsed request query string.
|
/// 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.
|
/// The remote address requesting this resource.
|
||||||
InternetAddress get remoteAddress => io.connectionInfo.remoteAddress;
|
InternetAddress get remoteAddress => io.connectionInfo.remoteAddress;
|
||||||
|
@ -106,6 +149,7 @@ class RequestContext extends Extensible {
|
||||||
.replaceAll(new RegExp(r'/+$'), '');
|
.replaceAll(new RegExp(r'/+$'), '');
|
||||||
ctx._io = request;
|
ctx._io = request;
|
||||||
|
|
||||||
|
if (app.lazyParseBodies != true)
|
||||||
ctx._body = (await parseBody(request,
|
ctx._body = (await parseBody(request,
|
||||||
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
|
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
|
||||||
{};
|
{};
|
||||||
|
@ -133,7 +177,39 @@ class RequestContext extends Extensible {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shorthand to add to [injections].
|
||||||
void inject(type, value) {
|
void inject(type, value) {
|
||||||
injections[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.
|
/// A convenience wrapper around an outgoing HTTP request.
|
||||||
class ResponseContext extends Extensible {
|
class ResponseContext extends Extensible {
|
||||||
|
final _LockableBytesBuilder _buffer = new _LockableBytesBuilder();
|
||||||
|
final Map<String, String> _headers = {HttpHeaders.SERVER: 'angel'};
|
||||||
bool _isOpen = true;
|
bool _isOpen = true;
|
||||||
|
|
||||||
/// The [Angel] instance that is sending a response.
|
/// The [Angel] instance that is sending a response.
|
||||||
|
@ -32,11 +34,27 @@ class ResponseContext extends Extensible {
|
||||||
final List<Cookie> cookies = [];
|
final List<Cookie> cookies = [];
|
||||||
|
|
||||||
/// Headers that will be sent to the user.
|
/// 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.
|
/// 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;
|
ResponseSerializer serializer = god.serialize;
|
||||||
|
|
||||||
/// This response's status code.
|
/// This response's status code.
|
||||||
|
@ -46,7 +64,7 @@ class ResponseContext extends Extensible {
|
||||||
bool get isOpen => _isOpen;
|
bool get isOpen => _isOpen;
|
||||||
|
|
||||||
/// A set of UTF-8 encoded bytes that will be written to the response.
|
/// 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.
|
/// Sets the status code to be sent with this response.
|
||||||
@Deprecated('Please use `statusCode=` instead.')
|
@Deprecated('Please use `statusCode=` instead.')
|
||||||
|
@ -91,8 +109,12 @@ class ResponseContext extends Extensible {
|
||||||
/// If `true`, all response finalizers will be skipped.
|
/// If `true`, all response finalizers will be skipped.
|
||||||
bool willCloseItself = false;
|
bool willCloseItself = false;
|
||||||
|
|
||||||
|
StateError _closed() => new StateError('Cannot modify a closed response.');
|
||||||
|
|
||||||
/// Sends a download as a response.
|
/// Sends a download as a response.
|
||||||
download(File file, {String filename}) async {
|
download(File file, {String filename}) async {
|
||||||
|
if (!_isOpen) throw _closed();
|
||||||
|
|
||||||
headers["Content-Disposition"] =
|
headers["Content-Disposition"] =
|
||||||
'attachment; filename="${filename ?? file.path}"';
|
'attachment; filename="${filename ?? file.path}"';
|
||||||
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(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.
|
/// Prevents more data from being written to the response.
|
||||||
void end() {
|
void end() {
|
||||||
|
_buffer._lock();
|
||||||
_isOpen = false;
|
_isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,14 +142,24 @@ class ResponseContext extends Extensible {
|
||||||
void json(value) => serialize(value, contentType: ContentType.JSON);
|
void json(value) => serialize(value, contentType: ContentType.JSON);
|
||||||
|
|
||||||
/// Returns a JSONP response.
|
/// Returns a JSONP response.
|
||||||
void jsonp(value, {String callbackName: "callback"}) {
|
void jsonp(value, {String callbackName: "callback", contentType}) {
|
||||||
write("$callbackName(${god.serialize(value)})");
|
if (!_isOpen) throw _closed();
|
||||||
headers[HttpHeaders.CONTENT_TYPE] = "application/javascript";
|
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();
|
end();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a view to the response stream, and closes the response.
|
/// Renders a view to the response stream, and closes the response.
|
||||||
Future render(String view, [Map data]) async {
|
Future render(String view, [Map data]) async {
|
||||||
|
if (!_isOpen) throw _closed();
|
||||||
write(await app.viewGenerator(view, data));
|
write(await app.viewGenerator(view, data));
|
||||||
headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString();
|
headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString();
|
||||||
end();
|
end();
|
||||||
|
@ -140,6 +173,7 @@ class ResponseContext extends Extensible {
|
||||||
///
|
///
|
||||||
/// See [Router]#navigate for more. :)
|
/// See [Router]#navigate for more. :)
|
||||||
void redirect(url, {bool absolute: true, int code: 302}) {
|
void redirect(url, {bool absolute: true, int code: 302}) {
|
||||||
|
if (!_isOpen) throw _closed();
|
||||||
headers[HttpHeaders.LOCATION] =
|
headers[HttpHeaders.LOCATION] =
|
||||||
url is String ? url : app.navigate(url, absolute: absolute);
|
url is String ? url : app.navigate(url, absolute: absolute);
|
||||||
statusCode = code ?? 302;
|
statusCode = code ?? 302;
|
||||||
|
@ -165,6 +199,7 @@ class ResponseContext extends Extensible {
|
||||||
|
|
||||||
/// Redirects to the given named [Route].
|
/// Redirects to the given named [Route].
|
||||||
void redirectTo(String name, [Map params, int code]) {
|
void redirectTo(String name, [Map params, int code]) {
|
||||||
|
if (!_isOpen) throw _closed();
|
||||||
Route _findRoute(Router r) {
|
Route _findRoute(Router r) {
|
||||||
for (Route route in r.routes) {
|
for (Route route in r.routes) {
|
||||||
if (route is SymlinkRoute) {
|
if (route is SymlinkRoute) {
|
||||||
|
@ -189,6 +224,7 @@ class ResponseContext extends Extensible {
|
||||||
|
|
||||||
/// Redirects to the given [Controller] action.
|
/// Redirects to the given [Controller] action.
|
||||||
void redirectToAction(String action, [Map params, int code]) {
|
void redirectToAction(String action, [Map params, int code]) {
|
||||||
|
if (!_isOpen) throw _closed();
|
||||||
// UserController@show
|
// UserController@show
|
||||||
List<String> split = action.split("@");
|
List<String> split = action.split("@");
|
||||||
|
|
||||||
|
@ -218,7 +254,7 @@ class ResponseContext extends Extensible {
|
||||||
/// Copies a file's contents into the response buffer.
|
/// Copies a file's contents into the response buffer.
|
||||||
Future sendFile(File file,
|
Future sendFile(File file,
|
||||||
{int chunkSize, int sleepMs: 0, bool resumable: true}) async {
|
{int chunkSize, int sleepMs: 0, bool resumable: true}) async {
|
||||||
if (!isOpen) return;
|
if (!_isOpen) throw _closed();
|
||||||
|
|
||||||
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
||||||
buffer.add(await file.readAsBytes());
|
buffer.add(await file.readAsBytes());
|
||||||
|
@ -229,6 +265,7 @@ class ResponseContext extends Extensible {
|
||||||
///
|
///
|
||||||
/// [contentType] can be either a [String], or a [ContentType].
|
/// [contentType] can be either a [String], or a [ContentType].
|
||||||
void serialize(value, {contentType}) {
|
void serialize(value, {contentType}) {
|
||||||
|
if (!_isOpen) throw _closed();
|
||||||
var text = serializer(value);
|
var text = serializer(value);
|
||||||
write(text);
|
write(text);
|
||||||
|
|
||||||
|
@ -247,7 +284,7 @@ class ResponseContext extends Extensible {
|
||||||
int sleepMs: 0,
|
int sleepMs: 0,
|
||||||
bool resumable: true,
|
bool resumable: true,
|
||||||
Codec<List<int>, List<int>> codec}) async {
|
Codec<List<int>, List<int>> codec}) async {
|
||||||
if (!isOpen) return;
|
if (!_isOpen) throw _closed();
|
||||||
|
|
||||||
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
||||||
end();
|
end();
|
||||||
|
@ -261,7 +298,9 @@ class ResponseContext extends Extensible {
|
||||||
|
|
||||||
/// Writes data to the response.
|
/// Writes data to the response.
|
||||||
void write(value, {Encoding encoding: UTF8}) {
|
void write(value, {Encoding encoding: UTF8}) {
|
||||||
if (isOpen) {
|
if (!_isOpen)
|
||||||
|
throw _closed();
|
||||||
|
else {
|
||||||
if (value is List<int>)
|
if (value is List<int>)
|
||||||
buffer.add(value);
|
buffer.add(value);
|
||||||
else
|
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.
|
/// A function that receives an incoming [RequestContext] and responds to it.
|
||||||
typedef Future RequestHandler(RequestContext req, ResponseContext res);
|
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.
|
/// return `true`. Works well with [Router].chain.
|
||||||
RequestMiddleware waterfall(List handlers) {
|
RequestMiddleware waterfall(List handlers) {
|
||||||
|
return (RequestContext req, res) async {
|
||||||
for (var handler in handlers) {
|
for (var handler in handlers) {
|
||||||
if (handler is! RequestMiddleware && handler is! RequestHandler)
|
var result = await req.app.executeHandler(handler, req, res);
|
||||||
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);
|
|
||||||
|
|
||||||
if (result != true) return result;
|
if (result != true) return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,9 @@ class Angel extends AngelBase {
|
||||||
/// `'production'`.
|
/// `'production'`.
|
||||||
bool get isProduction => Platform.environment['ANGEL_ENV'] == '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.
|
/// Fired whenever a controller is added to this instance.
|
||||||
///
|
///
|
||||||
/// **NOTE**: This is a broadcast stream.
|
/// **NOTE**: This is a broadcast stream.
|
||||||
|
@ -78,7 +81,8 @@ class Angel extends AngelBase {
|
||||||
|
|
||||||
/// Always run before responses are sent.
|
/// 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 = [];
|
final List<RequestHandler> responseFinalizers = [];
|
||||||
|
|
||||||
/// The handler currently configured to run on [AngelHttpException]s.
|
/// The handler currently configured to run on [AngelHttpException]s.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
library angel_framework.http.service;
|
library angel_framework.http.service;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:angel_framework/src/http/response_context.dart';
|
||||||
import 'package:merge_map/merge_map.dart';
|
import 'package:merge_map/merge_map.dart';
|
||||||
import '../util.dart';
|
import '../util.dart';
|
||||||
import 'angel_base.dart';
|
import 'angel_base.dart';
|
||||||
|
@ -100,15 +101,17 @@ class Service extends Routable {
|
||||||
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
||||||
|
|
||||||
Middleware createMiddleware = getAnnotation(this.create, Middleware);
|
Middleware createMiddleware = getAnnotation(this.create, Middleware);
|
||||||
post(
|
post('/', (req, ResponseContext res) async {
|
||||||
'/',
|
var r = await this.create(
|
||||||
(req, res) async => await this.create(
|
await req.lazyBody(),
|
||||||
req.body,
|
|
||||||
mergeMap([
|
mergeMap([
|
||||||
{'query': req.query},
|
{'query': req.query},
|
||||||
restProvider,
|
restProvider,
|
||||||
req.serviceParams
|
req.serviceParams
|
||||||
])),
|
]));
|
||||||
|
res.statusCode = 201;
|
||||||
|
return r;
|
||||||
|
},
|
||||||
middleware: []
|
middleware: []
|
||||||
..addAll(handlers)
|
..addAll(handlers)
|
||||||
..addAll(
|
..addAll(
|
||||||
|
@ -134,7 +137,7 @@ class Service extends Routable {
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res) async => await this.modify(
|
(req, res) async => await this.modify(
|
||||||
toId(req.params['id']),
|
toId(req.params['id']),
|
||||||
req.body,
|
await req.lazyBody(),
|
||||||
mergeMap([
|
mergeMap([
|
||||||
{'query': req.query},
|
{'query': req.query},
|
||||||
restProvider,
|
restProvider,
|
||||||
|
@ -150,7 +153,21 @@ class Service extends Routable {
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res) async => await this.update(
|
(req, res) async => await this.update(
|
||||||
toId(req.params['id']),
|
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([
|
mergeMap([
|
||||||
{'query': req.query},
|
{'query': req.query},
|
||||||
restProvider,
|
restProvider,
|
||||||
|
@ -162,6 +179,19 @@ class Service extends Routable {
|
||||||
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
||||||
|
|
||||||
Middleware removeMiddleware = getAnnotation(this.remove, Middleware);
|
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(
|
delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res) async => await this.remove(
|
(req, res) async => await this.remove(
|
||||||
|
@ -175,6 +205,10 @@ class Service extends Routable {
|
||||||
..addAll(handlers)
|
..addAll(handlers)
|
||||||
..addAll(
|
..addAll(
|
||||||
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
(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].
|
/// Invoked when this service is wrapped within a [HookedService].
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: angel_framework
|
name: angel_framework
|
||||||
version: 1.0.0-dev.68
|
version: 1.0.0-dev.69
|
||||||
description: Core libraries for the Angel framework.
|
description: Core libraries for the Angel framework.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_framework
|
homepage: https://github.com/angel-dart/angel_framework
|
||||||
|
|
|
@ -59,7 +59,7 @@ main() {
|
||||||
middleware: ['interceptor']);
|
middleware: ['interceptor']);
|
||||||
app.get('/hello', 'world');
|
app.get('/hello', 'world');
|
||||||
app.get('/name/:first/last/:last', (req, res) => req.params);
|
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.use('/todos/:id', todos);
|
||||||
app
|
app
|
||||||
.get('/greet/:name',
|
.get('/greet/:name',
|
||||||
|
|
|
@ -56,6 +56,7 @@ main() {
|
||||||
String postData = god.serialize({'text': 'Hello, world!'});
|
String postData = god.serialize({'text': 'Hello, world!'});
|
||||||
var response =
|
var response =
|
||||||
await client.post("$url/todos", headers: headers, body: postData);
|
await client.post("$url/todos", headers: headers, body: postData);
|
||||||
|
expect(response.statusCode, 201);
|
||||||
var json = god.deserialize(response.body);
|
var json = god.deserialize(response.body);
|
||||||
print(json);
|
print(json);
|
||||||
expect(json['text'], equals('Hello, world!'));
|
expect(json['text'], equals('Hello, world!'));
|
||||||
|
@ -65,6 +66,7 @@ main() {
|
||||||
String postData = god.serialize({'text': 'Hello, world!'});
|
String postData = god.serialize({'text': 'Hello, world!'});
|
||||||
await client.post("$url/todos", headers: headers, body: postData);
|
await client.post("$url/todos", headers: headers, body: postData);
|
||||||
var response = await client.get("$url/todos/0");
|
var response = await client.get("$url/todos/0");
|
||||||
|
expect(response.statusCode, 200);
|
||||||
var json = god.deserialize(response.body);
|
var json = god.deserialize(response.body);
|
||||||
print(json);
|
print(json);
|
||||||
expect(json['text'], equals('Hello, world!'));
|
expect(json['text'], equals('Hello, world!'));
|
||||||
|
@ -76,6 +78,7 @@ main() {
|
||||||
postData = god.serialize({'text': 'modified'});
|
postData = god.serialize({'text': 'modified'});
|
||||||
var response =
|
var response =
|
||||||
await client.patch("$url/todos/0", headers: headers, body: postData);
|
await client.patch("$url/todos/0", headers: headers, body: postData);
|
||||||
|
expect(response.statusCode, 200);
|
||||||
var json = god.deserialize(response.body);
|
var json = god.deserialize(response.body);
|
||||||
print(json);
|
print(json);
|
||||||
expect(json['text'], equals('modified'));
|
expect(json['text'], equals('modified'));
|
||||||
|
@ -87,6 +90,7 @@ main() {
|
||||||
postData = god.serialize({'over': 'write'});
|
postData = god.serialize({'over': 'write'});
|
||||||
var response =
|
var response =
|
||||||
await client.post("$url/todos/0", headers: headers, body: postData);
|
await client.post("$url/todos/0", headers: headers, body: postData);
|
||||||
|
expect(response.statusCode, 200);
|
||||||
var json = god.deserialize(response.body);
|
var json = god.deserialize(response.body);
|
||||||
print(json);
|
print(json);
|
||||||
expect(json['text'], equals(null));
|
expect(json['text'], equals(null));
|
||||||
|
@ -97,6 +101,7 @@ main() {
|
||||||
String postData = god.serialize({'text': 'Hello, world!'});
|
String postData = god.serialize({'text': 'Hello, world!'});
|
||||||
await client.post("$url/todos", headers: headers, body: postData);
|
await client.post("$url/todos", headers: headers, body: postData);
|
||||||
var response = await client.delete("$url/todos/0");
|
var response = await client.delete("$url/todos/0");
|
||||||
|
expect(response.statusCode, 200);
|
||||||
var json = god.deserialize(response.body);
|
var json = god.deserialize(response.body);
|
||||||
print(json);
|
print(json);
|
||||||
expect(json['text'], equals('Hello, world!'));
|
expect(json['text'], equals('Hello, world!'));
|
||||||
|
|
Loading…
Reference in a new issue