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:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:mirrors';
|
import 'dart:mirrors';
|
||||||
import 'package:body_parser/body_parser.dart';
|
import 'package:body_parser/body_parser.dart';
|
||||||
import 'package:json_god/json_god.dart';
|
import 'package:json_god/json_god.dart';
|
||||||
|
import 'package:merge_map/merge_map.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:route/server.dart';
|
|
||||||
|
|
||||||
part 'extensible.dart';
|
part 'extensible.dart';
|
||||||
|
part 'errors.dart';
|
||||||
part 'request_context.dart';
|
part 'request_context.dart';
|
||||||
part 'response_context.dart';
|
part 'response_context.dart';
|
||||||
part 'route.dart';
|
part 'route.dart';
|
||||||
|
|
|
@ -13,15 +13,6 @@ class Routable extends Extensible {
|
||||||
/// A set of [Service] objects that have been mapped into routes.
|
/// A set of [Service] objects that have been mapped into routes.
|
||||||
Map <Pattern, Service> services = {};
|
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.
|
/// Assigns a middleware to a name for convenience.
|
||||||
registerMiddleware(String name, Middleware middleware) {
|
registerMiddleware(String name, Middleware middleware) {
|
||||||
this.middleware[name] = middleware;
|
this.middleware[name] = middleware;
|
||||||
|
@ -35,6 +26,10 @@ class Routable extends Extensible {
|
||||||
middleware.addAll(routable.middleware);
|
middleware.addAll(routable.middleware);
|
||||||
for (Route route in routable.routes) {
|
for (Route route in routable.routes) {
|
||||||
Route provisional = new Route('', path);
|
Route provisional = new Route('', path);
|
||||||
|
if (route.path == '/') {
|
||||||
|
route.path = '';
|
||||||
|
route.matcher = new RegExp(r'^\/?$');
|
||||||
|
}
|
||||||
route.matcher = new RegExp(route.matcher.pattern.replaceAll(
|
route.matcher = new RegExp(route.matcher.pattern.replaceAll(
|
||||||
new RegExp('^\\^'),
|
new RegExp('^\\^'),
|
||||||
provisional.matcher.pattern.replaceAll(new RegExp(r'\$$'), '')));
|
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() {
|
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);
|
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
|
||||||
|
|
||||||
/// A function that configures an [Angel] server in some way.
|
/// 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.
|
/// A powerful real-time/REST/MVC server class.
|
||||||
class Angel extends Routable {
|
class Angel extends Routable {
|
||||||
ServerGenerator _serverGenerator = (address, port) async => await HttpServer
|
ServerGenerator _serverGenerator =
|
||||||
.bind(address, port);
|
(address, port) async => await HttpServer.bind(address, port);
|
||||||
var viewGenerator = (String view,
|
|
||||||
[Map data]) => "No view engine has been configured yet.";
|
/// 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;
|
HttpServer httpServer;
|
||||||
God god = new God();
|
God god = new God();
|
||||||
|
|
||||||
startServer(InternetAddress address, int port) async {
|
startServer(InternetAddress address, int port) async {
|
||||||
var server = await _serverGenerator(
|
var server =
|
||||||
address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port);
|
||||||
this.httpServer = server;
|
this.httpServer = server;
|
||||||
var router = new Router(server);
|
|
||||||
|
|
||||||
this.routes.forEach((Route route) {
|
server.listen((HttpRequest request) async {
|
||||||
router.serve(route.matcher, method: route.method).listen((
|
String req_url =
|
||||||
HttpRequest request) async {
|
request.uri.toString().replaceAll(new RegExp(r'\/+$'), '');
|
||||||
RequestContext req = await RequestContext.from(
|
RequestContext req = await RequestContext.from(request, {}, this, null);
|
||||||
request, route.parseParameters(request.uri.toString()), this,
|
ResponseContext res = await ResponseContext.from(request.response, this);
|
||||||
route);
|
|
||||||
ResponseContext res = await ResponseContext.from(
|
|
||||||
request.response, this);
|
|
||||||
bool canContinue = true;
|
|
||||||
|
|
||||||
for (var handler in route.handlers) {
|
bool canContinue = true;
|
||||||
if (canContinue) {
|
|
||||||
canContinue = await new Future<bool>.sync(() async {
|
var execHandler = (handler, req) async {
|
||||||
return _applyHandler(handler, req, res);
|
if (canContinue) {
|
||||||
}).catchError((e) {
|
canContinue = await new Future.sync(() async {
|
||||||
stderr.write(e.error);
|
return _applyHandler(handler, req, res);
|
||||||
canContinue = false;
|
}).catchError((e, [StackTrace stackTrace]) async {
|
||||||
return false;
|
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);
|
for (var handler in after) {
|
||||||
});
|
await execHandler(handler, req);
|
||||||
});
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
_finalizeResponse(request, res);
|
_finalizeResponse(request, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,39 +110,34 @@ class Angel extends Routable {
|
||||||
else if (result != null) {
|
else if (result != null) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
return false;
|
return false;
|
||||||
} else return true;
|
} else
|
||||||
|
return res.isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handler is RequestHandler) {
|
if (handler is RequestHandler) {
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
return res.isOpen;
|
return res.isOpen;
|
||||||
}
|
} else if (handler is RawRequestHandler) {
|
||||||
|
|
||||||
else if (handler is RawRequestHandler) {
|
|
||||||
var result = await handler(req.underlyingRequest);
|
var result = await handler(req.underlyingRequest);
|
||||||
if (result is bool)
|
if (result is bool)
|
||||||
return result == true;
|
return result == true;
|
||||||
else if (result != null) {
|
else if (result != null) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
return false;
|
return false;
|
||||||
} else return true;
|
} else
|
||||||
}
|
return true;
|
||||||
|
} else if (handler is Function || handler is Future) {
|
||||||
else if (handler is Function || handler is Future) {
|
|
||||||
var result = await handler();
|
var result = await handler();
|
||||||
if (result is bool)
|
if (result is bool)
|
||||||
return result == true;
|
return result == true;
|
||||||
else if (result != null) {
|
else if (result != null) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
return false;
|
return false;
|
||||||
} else return true;
|
} else
|
||||||
}
|
return true;
|
||||||
|
} else if (middleware.containsKey(handler)) {
|
||||||
else if (middleware.containsKey(handler)) {
|
|
||||||
return await _applyHandler(middleware[handler], req, res);
|
return await _applyHandler(middleware[handler], req, res);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
else {
|
|
||||||
res.willCloseItself = true;
|
res.willCloseItself = true;
|
||||||
res.underlyingResponse.write(god.serialize(handler));
|
res.underlyingResponse.write(god.serialize(handler));
|
||||||
await res.underlyingResponse.close();
|
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.
|
/// Applies an [AngelConfigurer] to this instance.
|
||||||
void configure(AngelConfigurer configurer) {
|
Future configure(AngelConfigurer configurer) async {
|
||||||
configurer(this);
|
await configurer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the server.
|
/// Starts the server.
|
||||||
void listen({InternetAddress address, int port: 3000}) {
|
void listen({InternetAddress address, int port: 3000}) {
|
||||||
runZoned(() async {
|
runZoned(() async {
|
||||||
await startServer(address, port);
|
await startServer(address, port);
|
||||||
}, onError: onError);
|
}, onError: _onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Responds to a 404.
|
@override
|
||||||
RequestHandler on404 = (req, res) => res.write("404 Not Found");
|
use(Pattern path, Routable routable) {
|
||||||
|
if (routable is Service) {
|
||||||
|
routable.app = this;
|
||||||
|
}
|
||||||
|
super.use(path, routable);
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(handler) {
|
||||||
|
_errorHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles a server error.
|
/// Handles a server error.
|
||||||
onError(e, [StackTrace stackTrace]) {
|
_onError(e, [StackTrace stackTrace]) {
|
||||||
stderr.write(e.toString());
|
stderr.write(e.toString());
|
||||||
if (stackTrace != null)
|
if (stackTrace != null) stderr.write(stackTrace.toString());
|
||||||
stderr.write(stackTrace.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Angel() : super() {}
|
Angel() : super() {}
|
||||||
|
|
||||||
/// Creates an HTTPS server.
|
/// 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.
|
/// A data store exposed to the Internet.
|
||||||
class Service extends Routable {
|
class Service extends Routable {
|
||||||
|
/// The [Angel] app powering this service.
|
||||||
|
Angel app;
|
||||||
|
|
||||||
/// Retrieves all resources.
|
/// Retrieves all resources.
|
||||||
Future<List> index([Map params]) {
|
Future<List> index([Map params]) {
|
||||||
throw new MethodNotAllowedError('find');
|
throw new AngelHttpException.MethodNotAllowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the desired resource.
|
/// Retrieves the desired resource.
|
||||||
Future<Object> read(id, [Map params]) {
|
Future<Object> read(id, [Map params]) {
|
||||||
throw new MethodNotAllowedError('get');
|
throw new AngelHttpException.MethodNotAllowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a resource.
|
/// Creates a resource.
|
||||||
Future<Object> create(Map data, [Map params]) {
|
Future<Object> create(Map data, [Map params]) {
|
||||||
throw new MethodNotAllowedError('create');
|
throw new AngelHttpException.MethodNotAllowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifies a resource.
|
/// 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]) {
|
Future<Object> update(id, Map data, [Map params]) {
|
||||||
throw new MethodNotAllowedError('update');
|
throw new AngelHttpException.MethodNotAllowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the given resource.
|
/// Removes the given resource.
|
||||||
Future<Object> remove(id, [Map params]) {
|
Future<Object> remove(id, [Map params]) {
|
||||||
throw new MethodNotAllowedError('remove');
|
throw new AngelHttpException.MethodNotAllowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
Service() : super() {
|
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 =>
|
get('/:id', (req, res) async =>
|
||||||
res.json(await this.read(req.params['id'], req.query)));
|
await this.read(req.params['id'], req.query));
|
||||||
post('/', (req, res) async => res.json(await this.create(req.body)));
|
|
||||||
post('/:id', (req, res) async =>
|
patch('/:id', (req, res) async => await this.modify(
|
||||||
res.json(await this.update(req.params['id'], req.body)));
|
req.params['id'], req.body));
|
||||||
delete('/:id', (req, res) async =>
|
|
||||||
res.json(await this.remove(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();
|
God god = new God();
|
||||||
Map <int, T> items = {};
|
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 {
|
Future<Object> create(Map data, [Map params]) async {
|
||||||
data['id'] = items.length;
|
try {
|
||||||
items[items.length] = god.deserializeFromMap(data, T);
|
items[items.length] = god.deserializeFromMap(data, T);
|
||||||
return items[items.length - 1];
|
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 {
|
Future<Object> update(id, Map data, [Map params]) async {
|
||||||
data['id'] = int.parse(id);
|
int desiredId = int.parse(id.toString());
|
||||||
items[int.parse(id)] = god.deserializeFromMap(data, T);
|
if (items.containsKey(desiredId)) {
|
||||||
return data;
|
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 {
|
Future<Object> remove(id, [Map params]) async {
|
||||||
var item = items[int.parse(id)];
|
int desiredId = int.parse(id.toString());
|
||||||
items.remove(int.parse(id));
|
if (items.containsKey(desiredId)) {
|
||||||
return item;
|
T item = items[desiredId];
|
||||||
|
items[desiredId] = null;
|
||||||
|
return makeJson(desiredId, item);
|
||||||
|
} else throw new AngelHttpException.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
MemoryService() : super();
|
MemoryService() : super();
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
name: angel_framework
|
name: angel_framework
|
||||||
version: 0.0.0-dev.5
|
version: 0.0.0-dev.6
|
||||||
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
|
||||||
dependencies:
|
dependencies:
|
||||||
body_parser: ">=1.0.0-dev <2.0.0"
|
body_parser: ">=1.0.0-dev <2.0.0"
|
||||||
json_god: ">=1.0.0 <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"
|
mime: ">=0.9.3 <0.10.0"
|
||||||
route: ">= 0.4.6 <0.5.0"
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
http: ">= 0.11.3 < 0.12.0"
|
http: ">= 0.11.3 < 0.12.0"
|
||||||
test: ">= 0.12.13 < 0.13.0"
|
test: ">= 0.12.13 < 0.13.0"
|
|
@ -29,10 +29,11 @@ main() {
|
||||||
angel.get('/intercepted', 'This should not be shown',
|
angel.get('/intercepted', 'This should not be shown',
|
||||||
middleware: ['interceptor']);
|
middleware: ['interceptor']);
|
||||||
angel.get('/hello', 'world');
|
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.post('/lambda', (req, res) => req.body);
|
||||||
angel.use('/nes', nested);
|
angel.use('/nes', nested);
|
||||||
angel.use('/todos/:id', todos);
|
angel.use('/todos/:id', todos);
|
||||||
|
angel.get('*', 'MJ');
|
||||||
|
|
||||||
client = new http.Client();
|
client = new http.Client();
|
||||||
await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
||||||
|
@ -87,5 +88,10 @@ main() {
|
||||||
"$url/lambda", headers: headers, body: postData);
|
"$url/lambda", headers: headers, body: postData);
|
||||||
expect(god.deserialize(response.body)['it'], equals('works'));
|
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';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
class Todo {
|
class Todo {
|
||||||
int id;
|
|
||||||
String text;
|
String text;
|
||||||
|
String over;
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
group('Services', () {
|
group('Services', () {
|
||||||
Map headers = {
|
Map headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
Angel angel;
|
Angel angel;
|
||||||
|
@ -38,17 +39,80 @@ main() {
|
||||||
group('memory', () {
|
group('memory', () {
|
||||||
test('can index an empty service', () async {
|
test('can index an empty service', () async {
|
||||||
var response = await client.get("$url/todos/");
|
var response = await client.get("$url/todos/");
|
||||||
|
print(response.body);
|
||||||
expect(response.body, equals('[]'));
|
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 {
|
test('can create data', () async {
|
||||||
String postData = god.serialize({'text': 'Hello, world!'});
|
String postData = god.serialize({'text': 'Hello, world!'});
|
||||||
var response = await client.post(
|
var response = await client.post(
|
||||||
"$url/todos/", headers: headers, body: postData);
|
"$url/todos", headers: headers, body: postData);
|
||||||
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!'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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