Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
This commit is contained in:
parent
c2de78db0c
commit
93a29c43cf
10 changed files with 425 additions and 118 deletions
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# angel_framework
|
||||
Documentation in the works.
|
96
lib/src/http/errors.dart
Normal file
96
lib/src/http/errors.dart
Normal file
|
@ -0,0 +1,96 @@
|
|||
part of angel_framework.http;
|
||||
|
||||
class _AngelHttpExceptionBase implements Exception {
|
||||
int statusCode;
|
||||
String message;
|
||||
List<String> errors;
|
||||
|
||||
_AngelHttpExceptionBase.base() {}
|
||||
|
||||
_AngelHttpExceptionBase(this.statusCode, this.message,
|
||||
{List<String> this.errors: const []});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$statusCode: $message";
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
'isError': true,
|
||||
'statusCode': statusCode,
|
||||
'message': message,
|
||||
'errors': errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Basically the same as
|
||||
/// [feathers-errors](https://github.com/feathersjs/feathers-errors).
|
||||
class AngelHttpException extends _AngelHttpExceptionBase {
|
||||
/// Throws a 500 Internal Server Error.
|
||||
/// Set includeRealException to true to print include the actual exception along with
|
||||
/// this error. Useful flag for development vs. production.
|
||||
AngelHttpException(Exception exception,
|
||||
{bool includeRealException: false, StackTrace stackTrace}) :super.base() {
|
||||
statusCode = 500;
|
||||
message = "500 Internal Server Error";
|
||||
if (includeRealException) {
|
||||
errors.add(exception.toString());
|
||||
if (stackTrace != null) {
|
||||
errors.add(stackTrace.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws a 400 Bad Request error, including an optional arrray of (validation?)
|
||||
/// errors you specify.
|
||||
AngelHttpException.BadRequest(
|
||||
{String message: '400 Bad Request', List<String> errors: const[]})
|
||||
: super(400, message, errors: errors);
|
||||
|
||||
/// Throws a 401 Not Authenticated error.
|
||||
AngelHttpException.NotAuthenticated({String message: '401 Not Authenticated'})
|
||||
: super(401, message);
|
||||
|
||||
/// Throws a 402 Payment Required error.
|
||||
AngelHttpException.PaymentRequired({String message: '402 Payment Required'})
|
||||
: super(402, message);
|
||||
|
||||
/// Throws a 403 Forbidden error.
|
||||
AngelHttpException.Forbidden({String message: '403 Forbidden'})
|
||||
: super(403, message);
|
||||
|
||||
/// Throws a 404 Not Found error.
|
||||
AngelHttpException.NotFound({String message: '404 Not Found'})
|
||||
: super(404, message);
|
||||
|
||||
/// Throws a 405 Method Not Allowed error.
|
||||
AngelHttpException.MethodNotAllowed(
|
||||
{String message: '405 Method Not Allowed'})
|
||||
: super(405, message);
|
||||
|
||||
/// Throws a 406 Not Acceptable error.
|
||||
AngelHttpException.NotAcceptable({String message: '406 Not Acceptable'})
|
||||
: super(406, message);
|
||||
|
||||
/// Throws a 408 Timeout error.
|
||||
AngelHttpException.MethodTimeout({String message: '408 Timeout'})
|
||||
: super(408, message);
|
||||
|
||||
/// Throws a 409 Conflict error.
|
||||
AngelHttpException.Conflict({String message: '409 Conflict'})
|
||||
: super(409, message);
|
||||
|
||||
/// Throws a 422 Not Processable error.
|
||||
AngelHttpException.NotProcessable({String message: '422 Not Processable'})
|
||||
: super(422, message);
|
||||
|
||||
/// Throws a 501 Not Implemented error.
|
||||
AngelHttpException.NotImplemented({String message: '501 Not Implemented'})
|
||||
: super(501, message);
|
||||
|
||||
/// Throws a 503 Unavailable error.
|
||||
AngelHttpException.Unavailable({String message: '503 Unavailable'})
|
||||
: super(503, message);
|
||||
}
|
|
@ -4,13 +4,15 @@ library angel_framework.http;
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:mirrors';
|
||||
import 'package:body_parser/body_parser.dart';
|
||||
import 'package:json_god/json_god.dart';
|
||||
import 'package:merge_map/merge_map.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:route/server.dart';
|
||||
|
||||
part 'extensible.dart';
|
||||
part 'errors.dart';
|
||||
part 'request_context.dart';
|
||||
part 'response_context.dart';
|
||||
part 'route.dart';
|
||||
|
|
|
@ -13,15 +13,6 @@ class Routable extends Extensible {
|
|||
/// A set of [Service] objects that have been mapped into routes.
|
||||
Map <Pattern, Service> services = {};
|
||||
|
||||
_makeRouteAssigner(String method) {
|
||||
return (Pattern path, Object handler, {List middleware}) {
|
||||
var route = new Route(method, path, (middleware ?? [])
|
||||
..add(handler));
|
||||
routes.add(route);
|
||||
return route;
|
||||
};
|
||||
}
|
||||
|
||||
/// Assigns a middleware to a name for convenience.
|
||||
registerMiddleware(String name, Middleware middleware) {
|
||||
this.middleware[name] = middleware;
|
||||
|
@ -35,6 +26,10 @@ class Routable extends Extensible {
|
|||
middleware.addAll(routable.middleware);
|
||||
for (Route route in routable.routes) {
|
||||
Route provisional = new Route('', path);
|
||||
if (route.path == '/') {
|
||||
route.path = '';
|
||||
route.matcher = new RegExp(r'^\/?$');
|
||||
}
|
||||
route.matcher = new RegExp(route.matcher.pattern.replaceAll(
|
||||
new RegExp('^\\^'),
|
||||
provisional.matcher.pattern.replaceAll(new RegExp(r'\$$'), '')));
|
||||
|
@ -48,13 +43,42 @@ class Routable extends Extensible {
|
|||
}
|
||||
}
|
||||
|
||||
RouteAssigner get, post, patch, delete;
|
||||
/// Adds a route that responds to the given path
|
||||
/// for requests with the given method (case-insensitive).
|
||||
/// Provide '*' as the method to respond to all methods.
|
||||
addRoute(String method, Pattern path, Object handler, {List middleware}) {
|
||||
var route = new Route(method.toUpperCase().trim(), path, (middleware ?? [])
|
||||
..add(handler));
|
||||
routes.add(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
/// Adds a route that responds to any request matching the given path.
|
||||
all(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('*', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a GET request.
|
||||
get(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('GET', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a POST request.
|
||||
post(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('POST', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a PATCH request.
|
||||
patch(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('PATCH', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a DELETE request.
|
||||
delete(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('DELETE', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
Routable() {
|
||||
this.get = _makeRouteAssigner('GET');
|
||||
this.post = _makeRouteAssigner('POST');
|
||||
this.patch = _makeRouteAssigner('PATCH');
|
||||
this.delete = _makeRouteAssigner('DELETE');
|
||||
}
|
||||
|
||||
}
|
|
@ -4,57 +4,97 @@ part of angel_framework.http;
|
|||
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
|
||||
|
||||
/// A function that configures an [Angel] server in some way.
|
||||
typedef AngelConfigurer(Angel app);
|
||||
typedef Future AngelConfigurer(Angel app);
|
||||
|
||||
/// A powerful real-time/REST/MVC server class.
|
||||
class Angel extends Routable {
|
||||
ServerGenerator _serverGenerator = (address, port) async => await HttpServer
|
||||
.bind(address, port);
|
||||
var viewGenerator = (String view,
|
||||
[Map data]) => "No view engine has been configured yet.";
|
||||
ServerGenerator _serverGenerator =
|
||||
(address, port) async => await HttpServer.bind(address, port);
|
||||
|
||||
/// Default error handler, show HTML error page
|
||||
var _errorHandler = (AngelHttpException e, req, ResponseContext res) {
|
||||
res.status(e.statusCode);
|
||||
res.write("<DOCTYPE html><html><head><title>${e.message}</title>");
|
||||
res.write("</head><body><h1>${e.message}</h1><ul>");
|
||||
for (String error in e.errors) {
|
||||
res.write("<li>$error</li>");
|
||||
}
|
||||
res.write("</ul></body></html>");
|
||||
res.end();
|
||||
};
|
||||
|
||||
var viewGenerator =
|
||||
(String view, [Map data]) => "No view engine has been configured yet.";
|
||||
|
||||
/// [Middleware] to be run before all requests.
|
||||
List before = [];
|
||||
|
||||
/// [Middleware] to be run after all requests.
|
||||
List after = [];
|
||||
|
||||
HttpServer httpServer;
|
||||
God god = new God();
|
||||
|
||||
startServer(InternetAddress address, int port) async {
|
||||
var server = await _serverGenerator(
|
||||
address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
||||
var server =
|
||||
await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
||||
this.httpServer = server;
|
||||
var router = new Router(server);
|
||||
|
||||
this.routes.forEach((Route route) {
|
||||
router.serve(route.matcher, method: route.method).listen((
|
||||
HttpRequest request) async {
|
||||
RequestContext req = await RequestContext.from(
|
||||
request, route.parseParameters(request.uri.toString()), this,
|
||||
route);
|
||||
ResponseContext res = await ResponseContext.from(
|
||||
request.response, this);
|
||||
bool canContinue = true;
|
||||
server.listen((HttpRequest request) async {
|
||||
String req_url =
|
||||
request.uri.toString().replaceAll(new RegExp(r'\/+$'), '');
|
||||
RequestContext req = await RequestContext.from(request, {}, this, null);
|
||||
ResponseContext res = await ResponseContext.from(request.response, this);
|
||||
|
||||
for (var handler in route.handlers) {
|
||||
if (canContinue) {
|
||||
canContinue = await new Future<bool>.sync(() async {
|
||||
return _applyHandler(handler, req, res);
|
||||
}).catchError((e) {
|
||||
stderr.write(e.error);
|
||||
canContinue = false;
|
||||
return false;
|
||||
});
|
||||
bool canContinue = true;
|
||||
|
||||
var execHandler = (handler, req) async {
|
||||
if (canContinue) {
|
||||
canContinue = await new Future.sync(() async {
|
||||
return _applyHandler(handler, req, res);
|
||||
}).catchError((e, [StackTrace stackTrace]) async {
|
||||
if (e is AngelHttpException) {
|
||||
// Special handling for AngelHttpExceptions :)
|
||||
try {
|
||||
String accept = request.headers.value(HttpHeaders.ACCEPT) ?? "*/*";
|
||||
if (accept == "*/*" ||
|
||||
accept.contains("application/json") ||
|
||||
accept.contains("application/javascript")) {
|
||||
res.json(e.toMap());
|
||||
} else {
|
||||
await _applyHandler(_errorHandler, req, res);
|
||||
}
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
_onError(e, stackTrace);
|
||||
canContinue = false;
|
||||
return false;
|
||||
});
|
||||
} else
|
||||
return false;
|
||||
};
|
||||
|
||||
for (var handler in before) {
|
||||
await execHandler(handler, req);
|
||||
}
|
||||
|
||||
for (Route route in routes) {
|
||||
if (!canContinue) break;
|
||||
if (route.matcher.hasMatch(req_url) &&
|
||||
(request.method == route.method || route.method == '*')) {
|
||||
req.params = route.parseParameters(request.uri.toString());
|
||||
req.route = route;
|
||||
|
||||
for (var handler in route.handlers) {
|
||||
await execHandler(handler, req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_finalizeResponse(request, res);
|
||||
});
|
||||
});
|
||||
|
||||
router.defaultStream.listen((HttpRequest request) async {
|
||||
RequestContext req = await RequestContext.from(
|
||||
request, {}, this,
|
||||
null);
|
||||
ResponseContext res = await ResponseContext.from(
|
||||
request.response, this);
|
||||
on404(req, res);
|
||||
for (var handler in after) {
|
||||
await execHandler(handler, req);
|
||||
}
|
||||
_finalizeResponse(request, res);
|
||||
});
|
||||
|
||||
|
@ -70,39 +110,34 @@ class Angel extends Routable {
|
|||
else if (result != null) {
|
||||
res.json(result);
|
||||
return false;
|
||||
} else return true;
|
||||
} else
|
||||
return res.isOpen;
|
||||
}
|
||||
|
||||
if (handler is RequestHandler) {
|
||||
await handler(req, res);
|
||||
return res.isOpen;
|
||||
}
|
||||
|
||||
else if (handler is RawRequestHandler) {
|
||||
} else if (handler is RawRequestHandler) {
|
||||
var result = await handler(req.underlyingRequest);
|
||||
if (result is bool)
|
||||
return result == true;
|
||||
else if (result != null) {
|
||||
res.json(result);
|
||||
return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
else if (handler is Function || handler is Future) {
|
||||
} else
|
||||
return true;
|
||||
} else if (handler is Function || handler is Future) {
|
||||
var result = await handler();
|
||||
if (result is bool)
|
||||
return result == true;
|
||||
else if (result != null) {
|
||||
res.json(result);
|
||||
return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
else if (middleware.containsKey(handler)) {
|
||||
} else
|
||||
return true;
|
||||
} else if (middleware.containsKey(handler)) {
|
||||
return await _applyHandler(middleware[handler], req, res);
|
||||
}
|
||||
|
||||
else {
|
||||
} else {
|
||||
res.willCloseItself = true;
|
||||
res.underlyingResponse.write(god.serialize(handler));
|
||||
await res.underlyingResponse.close();
|
||||
|
@ -117,30 +152,64 @@ class Angel extends Routable {
|
|||
}
|
||||
}
|
||||
|
||||
String _randomString(int length) {
|
||||
var rand = new Random();
|
||||
var codeUnits = new List.generate(length, (index) {
|
||||
return rand.nextInt(33) + 89;
|
||||
});
|
||||
|
||||
return new String.fromCharCodes(codeUnits);
|
||||
}
|
||||
|
||||
/// Applies an [AngelConfigurer] to this instance.
|
||||
void configure(AngelConfigurer configurer) {
|
||||
configurer(this);
|
||||
Future configure(AngelConfigurer configurer) async {
|
||||
await configurer(this);
|
||||
}
|
||||
|
||||
/// Starts the server.
|
||||
void listen({InternetAddress address, int port: 3000}) {
|
||||
runZoned(() async {
|
||||
await startServer(address, port);
|
||||
}, onError: onError);
|
||||
}, onError: _onError);
|
||||
}
|
||||
|
||||
/// Responds to a 404.
|
||||
RequestHandler on404 = (req, res) => res.write("404 Not Found");
|
||||
@override
|
||||
use(Pattern path, Routable routable) {
|
||||
if (routable is Service) {
|
||||
routable.app = this;
|
||||
}
|
||||
super.use(path, routable);
|
||||
}
|
||||
|
||||
onError(handler) {
|
||||
_errorHandler = handler;
|
||||
}
|
||||
|
||||
/// Handles a server error.
|
||||
onError(e, [StackTrace stackTrace]) {
|
||||
_onError(e, [StackTrace stackTrace]) {
|
||||
stderr.write(e.toString());
|
||||
if (stackTrace != null)
|
||||
stderr.write(stackTrace.toString());
|
||||
if (stackTrace != null) stderr.write(stackTrace.toString());
|
||||
}
|
||||
|
||||
Angel() : super() {}
|
||||
|
||||
/// Creates an HTTPS server.
|
||||
Angel.secure() : super() {}
|
||||
/// Provide paths to a certificate chain and server key (both .pem).
|
||||
/// If no password is provided, a random one will be generated upon running
|
||||
/// the server.
|
||||
Angel.secure(String certificateChainPath, String serverKeyPath,
|
||||
{String password})
|
||||
: super() {
|
||||
_serverGenerator = (InternetAddress address, int port) async {
|
||||
var certificateChain =
|
||||
Platform.script.resolve('server_chain.pem').toFilePath();
|
||||
var serverKey = Platform.script.resolve('server_key.pem').toFilePath();
|
||||
var serverContext = new SecurityContext();
|
||||
serverContext.useCertificateChain(certificateChain);
|
||||
serverContext.usePrivateKey(serverKey,
|
||||
password: password ?? _randomString(8));
|
||||
|
||||
return await HttpServer.bindSecure(address, port, serverContext);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -2,53 +2,53 @@ part of angel_framework.http;
|
|||
|
||||
/// A data store exposed to the Internet.
|
||||
class Service extends Routable {
|
||||
/// The [Angel] app powering this service.
|
||||
Angel app;
|
||||
|
||||
/// Retrieves all resources.
|
||||
Future<List> index([Map params]) {
|
||||
throw new MethodNotAllowedError('find');
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Retrieves the desired resource.
|
||||
Future<Object> read(id, [Map params]) {
|
||||
throw new MethodNotAllowedError('get');
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Creates a resource.
|
||||
Future<Object> create(Map data, [Map params]) {
|
||||
throw new MethodNotAllowedError('create');
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Modifies a resource.
|
||||
Future<Object> modify(id, Map data, [Map params]) {
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Overwrites a resource.
|
||||
Future<Object> update(id, Map data, [Map params]) {
|
||||
throw new MethodNotAllowedError('update');
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Removes the given resource.
|
||||
Future<Object> remove(id, [Map params]) {
|
||||
throw new MethodNotAllowedError('remove');
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
Service() : super() {
|
||||
get('/', (req, res) async => res.json(await this.index(req.query)));
|
||||
get('/', (req, res) async => await this.index(req.query));
|
||||
|
||||
post('/', (req, res) async => await this.create(req.body));
|
||||
|
||||
get('/:id', (req, res) async =>
|
||||
res.json(await this.read(req.params['id'], req.query)));
|
||||
post('/', (req, res) async => res.json(await this.create(req.body)));
|
||||
post('/:id', (req, res) async =>
|
||||
res.json(await this.update(req.params['id'], req.body)));
|
||||
delete('/:id', (req, res) async =>
|
||||
res.json(await this.remove(req.params['id'], req.body)));
|
||||
await this.read(req.params['id'], req.query));
|
||||
|
||||
patch('/:id', (req, res) async => await this.modify(
|
||||
req.params['id'], req.body));
|
||||
|
||||
post('/:id', (req, res) async => await this.update(
|
||||
req.params['id'], req.body));
|
||||
|
||||
delete('/:id', (req, res) async => await this.remove(req.params['id'], req.query));
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown when an unimplemented method is called.
|
||||
class MethodNotAllowedError extends Error {
|
||||
/// The action that threw the error.
|
||||
///
|
||||
/// Ex. 'get', 'remove'
|
||||
String action;
|
||||
|
||||
/// A description of this error.
|
||||
String get error => 'This service does not support the "$action" action.';
|
||||
|
||||
MethodNotAllowedError(String this.action);
|
||||
}
|
|
@ -5,26 +5,70 @@ class MemoryService<T> extends Service {
|
|||
God god = new God();
|
||||
Map <int, T> items = {};
|
||||
|
||||
Future<List> index([Map params]) async => items.values.toList();
|
||||
Map makeJson(int index, T t) {
|
||||
return mergeMap([god.serializeToMap(t), {'id': index}]);
|
||||
}
|
||||
|
||||
Future<Object> read(id, [Map params]) async => items[int.parse(id)];
|
||||
Future<List> index([Map params]) async {
|
||||
return items.keys
|
||||
.where((index) => items[index] != null)
|
||||
.map((index) => makeJson(index, items[index]))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<Object> read(id, [Map params]) async {
|
||||
int desiredId = int.parse(id.toString());
|
||||
if (items.containsKey(desiredId)) {
|
||||
T found = items[desiredId];
|
||||
if (found != null) {
|
||||
return makeJson(desiredId, found);
|
||||
} else throw new AngelHttpException.NotFound();
|
||||
} else throw new AngelHttpException.NotFound();
|
||||
}
|
||||
|
||||
Future<Object> create(Map data, [Map params]) async {
|
||||
data['id'] = items.length;
|
||||
items[items.length] = god.deserializeFromMap(data, T);
|
||||
return items[items.length - 1];
|
||||
try {
|
||||
items[items.length] = god.deserializeFromMap(data, T);
|
||||
T created = items[items.length - 1];
|
||||
return makeJson(items.length - 1, created);
|
||||
} catch (e) {
|
||||
throw new AngelHttpException.BadRequest(message: 'Invalid data.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Object> modify(id, Map data, [Map params]) async {
|
||||
int desiredId = int.parse(id.toString());
|
||||
if (items.containsKey(desiredId)) {
|
||||
try {
|
||||
Map existing = god.serializeToMap(items[desiredId]);
|
||||
data = mergeMap([existing, data]);
|
||||
items[desiredId] = god.deserializeFromMap(data, T);
|
||||
return makeJson(desiredId, items[desiredId]);
|
||||
} catch (e) {
|
||||
throw new AngelHttpException.BadRequest(message: 'Invalid data.');
|
||||
}
|
||||
} else throw new AngelHttpException.NotFound();
|
||||
}
|
||||
|
||||
Future<Object> update(id, Map data, [Map params]) async {
|
||||
data['id'] = int.parse(id);
|
||||
items[int.parse(id)] = god.deserializeFromMap(data, T);
|
||||
return data;
|
||||
int desiredId = int.parse(id.toString());
|
||||
if (items.containsKey(desiredId)) {
|
||||
try {
|
||||
items[desiredId] = god.deserializeFromMap(data, T);
|
||||
return makeJson(desiredId, items[desiredId]);
|
||||
} catch (e) {
|
||||
throw new AngelHttpException.BadRequest(message: 'Invalid data.');
|
||||
}
|
||||
} else throw new AngelHttpException.NotFound();
|
||||
}
|
||||
|
||||
Future<Object> remove(id, [Map params]) async {
|
||||
var item = items[int.parse(id)];
|
||||
items.remove(int.parse(id));
|
||||
return item;
|
||||
int desiredId = int.parse(id.toString());
|
||||
if (items.containsKey(desiredId)) {
|
||||
T item = items[desiredId];
|
||||
items[desiredId] = null;
|
||||
return makeJson(desiredId, item);
|
||||
} else throw new AngelHttpException.NotFound();
|
||||
}
|
||||
|
||||
MemoryService() : super();
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
name: angel_framework
|
||||
version: 0.0.0-dev.5
|
||||
version: 0.0.0-dev.6
|
||||
description: Core libraries for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_framework
|
||||
dependencies:
|
||||
body_parser: ">=1.0.0-dev <2.0.0"
|
||||
json_god: ">=1.0.0 <2.0.0"
|
||||
merge_map: ">=1.0.0 <2.0.0"
|
||||
mime: ">=0.9.3 <0.10.0"
|
||||
route: ">= 0.4.6 <0.5.0"
|
||||
dev_dependencies:
|
||||
http: ">= 0.11.3 < 0.12.0"
|
||||
test: ">= 0.12.13 < 0.13.0"
|
|
@ -29,10 +29,11 @@ main() {
|
|||
angel.get('/intercepted', 'This should not be shown',
|
||||
middleware: ['interceptor']);
|
||||
angel.get('/hello', 'world');
|
||||
angel.get('/name/:first/last/:last', (req, res) => res.json(req.params));
|
||||
angel.get('/name/:first/last/:last', (req, res) => req.params);
|
||||
angel.post('/lambda', (req, res) => req.body);
|
||||
angel.use('/nes', nested);
|
||||
angel.use('/todos/:id', todos);
|
||||
angel.get('*', 'MJ');
|
||||
|
||||
client = new http.Client();
|
||||
await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
||||
|
@ -87,5 +88,10 @@ main() {
|
|||
"$url/lambda", headers: headers, body: postData);
|
||||
expect(god.deserialize(response.body)['it'], equals('works'));
|
||||
});
|
||||
|
||||
test('Fallback routes', () async {
|
||||
var response = await client.get('$url/my_favorite_artist');
|
||||
expect(response.body, equals('"MJ"'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -4,13 +4,14 @@ import 'package:json_god/json_god.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
class Todo {
|
||||
int id;
|
||||
String text;
|
||||
String over;
|
||||
}
|
||||
|
||||
main() {
|
||||
group('Services', () {
|
||||
Map headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
Angel angel;
|
||||
|
@ -38,17 +39,80 @@ main() {
|
|||
group('memory', () {
|
||||
test('can index an empty service', () async {
|
||||
var response = await client.get("$url/todos/");
|
||||
print(response.body);
|
||||
expect(response.body, equals('[]'));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post(
|
||||
"$url/todos", headers: headers, body: postData);
|
||||
}
|
||||
response = await client.get("$url/todos");
|
||||
print(response.body);
|
||||
expect(god
|
||||
.deserialize(response.body)
|
||||
.length, equals(3));
|
||||
});
|
||||
|
||||
test('can create data', () async {
|
||||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
var response = await client.post(
|
||||
"$url/todos/", headers: headers, body: postData);
|
||||
"$url/todos", headers: headers, body: postData);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('Hello, world!'));
|
||||
});
|
||||
|
||||
test('can fetch data', () async {
|
||||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post(
|
||||
"$url/todos", headers: headers, body: postData);
|
||||
var response = await client.get(
|
||||
"$url/todos/0");
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('Hello, world!'));
|
||||
});
|
||||
|
||||
test('can modify data', () async {
|
||||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post(
|
||||
"$url/todos", headers: headers, body: postData);
|
||||
postData = god.serialize({'text': 'modified'});
|
||||
var response = await client.patch(
|
||||
"$url/todos/0", headers: headers, body: postData);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('modified'));
|
||||
});
|
||||
|
||||
test('can overwrite data', () async {
|
||||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post(
|
||||
"$url/todos", headers: headers, body: postData);
|
||||
postData = god.serialize({'over': 'write'});
|
||||
var response = await client.post(
|
||||
"$url/todos/0", headers: headers, body: postData);
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals(null));
|
||||
expect(json['over'], equals('write'));
|
||||
});
|
||||
|
||||
test('can delete data', () async {
|
||||
String postData = god.serialize({'text': 'Hello, world!'});
|
||||
await client.post(
|
||||
"$url/todos", headers: headers, body: postData);
|
||||
var response = await client.delete(
|
||||
"$url/todos/0");
|
||||
var json = god.deserialize(response.body);
|
||||
print(json);
|
||||
expect(json['text'], equals('Hello, world!'));
|
||||
response = await client.get("$url/todos");
|
||||
print(response.body);
|
||||
expect(god
|
||||
.deserialize(response.body)
|
||||
.length, equals(0));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue