@Middleware, @Hooked, .use-> services, middleware
This commit is contained in:
parent
496ecd8757
commit
f58b2dc259
13 changed files with 255 additions and 33 deletions
|
@ -13,10 +13,12 @@ import 'package:mime/mime.dart';
|
|||
|
||||
part 'extensible.dart';
|
||||
part 'errors.dart';
|
||||
part 'metadata/metadata.dart';
|
||||
part 'request_context.dart';
|
||||
part 'response_context.dart';
|
||||
part 'route.dart';
|
||||
part 'routable.dart';
|
||||
part 'server.dart';
|
||||
part 'service.dart';
|
||||
part 'service_hooked.dart';
|
||||
part 'services/memory.dart';
|
13
lib/src/http/metadata/metadata.dart
Normal file
13
lib/src/http/metadata/metadata.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
part of angel_framework.http;
|
||||
|
||||
/// Maps the given middleware(s) onto this handler.
|
||||
class Middleware {
|
||||
final List handlers;
|
||||
|
||||
const Middleware(List this.handlers);
|
||||
}
|
||||
|
||||
/// This service will send an event after every action.
|
||||
class Hooked {
|
||||
const Hooked();
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
part of angel_framework.http;
|
||||
|
||||
/// A function that intercepts a request and determines whether handling of it should continue.
|
||||
typedef Future<bool> Middleware(RequestContext req, ResponseContext res);
|
||||
typedef Future<bool> RequestMiddleware(RequestContext req, ResponseContext res);
|
||||
|
||||
/// A function that receives an incoming [RequestContext] and responds to it.
|
||||
typedef Future RequestHandler(RequestContext req, ResponseContext res);
|
||||
|
|
|
@ -74,7 +74,7 @@ class ResponseContext extends Extensible {
|
|||
/// Redirects to user to the given URL.
|
||||
redirect(String url, {int code: 301}) {
|
||||
header(HttpHeaders.LOCATION, url);
|
||||
status(code);
|
||||
status(code ?? 301);
|
||||
write('''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -95,6 +95,16 @@ class ResponseContext extends Extensible {
|
|||
end();
|
||||
}
|
||||
|
||||
/// Redirects to the given named [Route].
|
||||
redirectTo(String name, [Map params, int code]) {
|
||||
Route matched = app.routes.firstWhere((Route route) => route.name == name);
|
||||
if (matched != null) {
|
||||
return redirect(matched.makeUri(params), code: code);
|
||||
}
|
||||
|
||||
throw new ArgumentError.notNull('Route to redirect to ($name)');
|
||||
}
|
||||
|
||||
/// Streams a file to this response as chunked data.
|
||||
///
|
||||
/// Useful for video sites.
|
||||
|
|
|
@ -2,10 +2,34 @@ part of angel_framework.http;
|
|||
|
||||
typedef Route RouteAssigner(Pattern path, handler, {List middleware});
|
||||
|
||||
_matchingAnnotation(List<InstanceMirror> metadata, Type T) {
|
||||
for (InstanceMirror metaDatum in metadata) {
|
||||
if (metaDatum.hasReflectee) {
|
||||
var reflectee = metaDatum.reflectee;
|
||||
if (reflectee.runtimeType == T) {
|
||||
return reflectee;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_getAnnotation(obj, Type T) {
|
||||
if (obj is Function || obj is Future) {
|
||||
MethodMirror methodMirror = (reflect(obj) as ClosureMirror).function;
|
||||
return _matchingAnnotation(methodMirror.metadata, T);
|
||||
} else {
|
||||
ClassMirror classMirror = reflectClass(obj.runtimeType);
|
||||
return _matchingAnnotation(classMirror.metadata, T);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// A routable server that can handle dynamic requests.
|
||||
class Routable extends Extensible {
|
||||
/// Additional filters to be run on designated requests.
|
||||
Map <String, Middleware> middleware = {};
|
||||
Map <String, RequestMiddleware> requestMiddleware = {};
|
||||
|
||||
/// Dynamic request paths that this server will respond to.
|
||||
List<Route> routes = [];
|
||||
|
@ -14,16 +38,26 @@ class Routable extends Extensible {
|
|||
Map <Pattern, Service> services = {};
|
||||
|
||||
/// Assigns a middleware to a name for convenience.
|
||||
registerMiddleware(String name, Middleware middleware) {
|
||||
this.middleware[name] = middleware;
|
||||
registerMiddleware(String name, RequestMiddleware middleware) {
|
||||
this.requestMiddleware[name] = middleware;
|
||||
}
|
||||
|
||||
/// Retrieves the service assigned to the given path.
|
||||
Service service(Pattern path) => services[path];
|
||||
|
||||
/// Incorporates another routable's routes into this one's.
|
||||
use(Pattern path, Routable routable) {
|
||||
middleware.addAll(routable.middleware);
|
||||
/// Incorporates another [Routable]'s routes into this one's.
|
||||
///
|
||||
/// If `hooked` is set to `true` and a [Service] is provided,
|
||||
/// then that service will be wired to a [HookedService] proxy.
|
||||
/// If a `middlewareNamespace` is provided, then any middleware
|
||||
/// from the provided [Routable] will be prefixed by that namespace,
|
||||
/// with a dot.
|
||||
/// For example, if the [Routable] has a middleware 'y', and the `middlewareNamespace`
|
||||
/// is 'x', then that middleware will be available as 'x.y' in the main application.
|
||||
/// These namespaces can be nested.
|
||||
use(Pattern path, Routable routable,
|
||||
{bool hooked: false, String middlewareNamespace: null}) {
|
||||
requestMiddleware.addAll(routable.requestMiddleware);
|
||||
for (Route route in routable.routes) {
|
||||
Route provisional = new Route('', path);
|
||||
if (route.path == '/') {
|
||||
|
@ -38,43 +72,77 @@ class Routable extends Extensible {
|
|||
routes.add(route);
|
||||
}
|
||||
|
||||
// Let's copy middleware, heeding the optional middleware namespace.
|
||||
String middlewarePrefix = "";
|
||||
if (middlewareNamespace != null)
|
||||
middlewarePrefix = "$middlewareNamespace.";
|
||||
|
||||
for (String middlewareName in routable.requestMiddleware.keys) {
|
||||
requestMiddleware["$middlewarePrefix$middlewareName"] =
|
||||
routable.requestMiddleware[middlewareName];
|
||||
}
|
||||
|
||||
// Copy services, too. :)
|
||||
for (Pattern servicePath in routable.services.keys) {
|
||||
String newServicePath = path.toString().trim().replaceAll(
|
||||
new RegExp(r'(^\/+)|(\/+$)'), '') + '/$servicePath';
|
||||
services[newServicePath] = routable.services[servicePath];
|
||||
}
|
||||
|
||||
if (routable is Service) {
|
||||
services[path.toString().trim().replaceAll(new RegExp(r'(^\/+)|(\/+$)'), '')] = routable;
|
||||
Hooked hookedDeclaration = _getAnnotation(routable, Hooked);
|
||||
Service service = (hookedDeclaration != null || hooked)
|
||||
? new HookedService(routable)
|
||||
: routable;
|
||||
services[path.toString().trim().replaceAll(
|
||||
new RegExp(r'(^\/+)|(\/+$)'), '')] = service;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
Route addRoute(String method, Pattern path, Object handler,
|
||||
{List middleware}) {
|
||||
List handlers = [];
|
||||
|
||||
// Merge @Middleware declaration, if any
|
||||
Middleware middlewareDeclaration = _getAnnotation(
|
||||
handler, Middleware);
|
||||
if (middlewareDeclaration != null) {
|
||||
handlers.addAll(middlewareDeclaration.handlers);
|
||||
}
|
||||
|
||||
handlers
|
||||
..addAll(middleware ?? [])
|
||||
..add(handler);
|
||||
var route = new Route(method.toUpperCase().trim(), path, handlers);
|
||||
routes.add(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
/// Adds a route that responds to any request matching the given path.
|
||||
all(Pattern path, Object handler, {List middleware}) {
|
||||
Route 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}) {
|
||||
Route 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}) {
|
||||
Route 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}) {
|
||||
Route 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}) {
|
||||
Route delete(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('DELETE', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,23 @@ class Route {
|
|||
}
|
||||
}
|
||||
|
||||
/// Assigns a name to this Route.
|
||||
as(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
String makeUri([Map<String, dynamic> params]) {
|
||||
String result = path;
|
||||
if (params != null) {
|
||||
for (String key in (params.keys)) {
|
||||
result = result.replaceAll(new RegExp(":$key"), params[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parseParameters(String requestPath) {
|
||||
Map result = {};
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@ part of angel_framework.http;
|
|||
/// A function that binds an [Angel] server to an Internet address and port.
|
||||
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
|
||||
|
||||
/// Handles an [AngelHttpException].
|
||||
typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req,
|
||||
ResponseContext res);
|
||||
|
||||
/// A function that configures an [Angel] server in some way.
|
||||
typedef Future AngelConfigurer(Angel app);
|
||||
|
||||
|
@ -27,10 +31,10 @@ class Angel extends Routable {
|
|||
var viewGenerator =
|
||||
(String view, [Map data]) => "No view engine has been configured yet.";
|
||||
|
||||
/// [Middleware] to be run before all requests.
|
||||
/// [RequestMiddleware] to be run before all requests.
|
||||
List before = [];
|
||||
|
||||
/// [Middleware] to be run after all requests.
|
||||
/// [RequestMiddleware] to be run after all requests.
|
||||
List after = [];
|
||||
|
||||
HttpServer httpServer;
|
||||
|
@ -106,7 +110,7 @@ class Angel extends Routable {
|
|||
|
||||
Future<bool> _applyHandler(handler, RequestContext req,
|
||||
ResponseContext res) async {
|
||||
if (handler is Middleware) {
|
||||
if (handler is RequestMiddleware) {
|
||||
var result = await handler(req, res);
|
||||
if (result is bool)
|
||||
return result == true;
|
||||
|
@ -138,8 +142,8 @@ class Angel extends Routable {
|
|||
return false;
|
||||
} else
|
||||
return true;
|
||||
} else if (middleware.containsKey(handler)) {
|
||||
return await _applyHandler(middleware[handler], req, res);
|
||||
} else if (requestMiddleware.containsKey(handler)) {
|
||||
return await _applyHandler(requestMiddleware[handler], req, res);
|
||||
} else {
|
||||
res.willCloseItself = true;
|
||||
res.underlyingResponse.write(god.serialize(handler));
|
||||
|
@ -181,11 +185,13 @@ class Angel extends Routable {
|
|||
}
|
||||
|
||||
@override
|
||||
use(Pattern path, Routable routable) {
|
||||
use(Pattern path, Routable routable,
|
||||
{bool hooked: false, String middlewareNamespace: null}) {
|
||||
if (routable is Service) {
|
||||
routable.app = this;
|
||||
}
|
||||
super.use(path, routable);
|
||||
super.use(
|
||||
path, routable, hooked: hooked, middlewareNamespace: middlewareNamespace);
|
||||
}
|
||||
|
||||
onError(handler) {
|
||||
|
|
|
@ -11,27 +11,27 @@ class Service extends Routable {
|
|||
}
|
||||
|
||||
/// Retrieves the desired resource.
|
||||
Future<Object> read(id, [Map params]) {
|
||||
Future read(id, [Map params]) {
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Creates a resource.
|
||||
Future<Object> create(Map data, [Map params]) {
|
||||
Future create(Map data, [Map params]) {
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Modifies a resource.
|
||||
Future<Object> modify(id, Map data, [Map params]) {
|
||||
Future modify(id, Map data, [Map params]) {
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Overwrites a resource.
|
||||
Future<Object> update(id, Map data, [Map params]) {
|
||||
Future update(id, Map data, [Map params]) {
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
/// Removes the given resource.
|
||||
Future<Object> remove(id, [Map params]) {
|
||||
Future remove(id, [Map params]) {
|
||||
throw new AngelHttpException.MethodNotAllowed();
|
||||
}
|
||||
|
||||
|
|
73
lib/src/http/service_hooked.dart
Normal file
73
lib/src/http/service_hooked.dart
Normal file
|
@ -0,0 +1,73 @@
|
|||
part of angel_framework.http;
|
||||
|
||||
/// Wraps another service in a service that fires events on actions.
|
||||
class HookedService extends Service {
|
||||
StreamController<List> _onIndexed = new StreamController<List>();
|
||||
StreamController _onRead = new StreamController();
|
||||
StreamController _onCreated = new StreamController();
|
||||
StreamController _onModified = new StreamController();
|
||||
StreamController _onUpdated = new StreamController();
|
||||
StreamController _onRemoved = new StreamController();
|
||||
|
||||
Stream<List> get onIndexed => _onIndexed.stream;
|
||||
|
||||
Stream get onRead => _onRead.stream;
|
||||
|
||||
Stream get onCreated => _onCreated.stream;
|
||||
|
||||
Stream get onModified => _onModified.stream;
|
||||
|
||||
Stream get onUpdated => _onUpdated.stream;
|
||||
|
||||
Stream get onRemoved => _onRemoved.stream;
|
||||
|
||||
final Service inner;
|
||||
|
||||
HookedService(Service this.inner);
|
||||
|
||||
|
||||
@override
|
||||
Future<List> index([Map params]) async {
|
||||
List indexed = await inner.index(params);
|
||||
_onIndexed.add(indexed);
|
||||
return indexed;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future read(id, [Map params]) async {
|
||||
var retrieved = await inner.read(id, params);
|
||||
_onRead.add(retrieved);
|
||||
return retrieved;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future create(Map data, [Map params]) async {
|
||||
var created = await inner.create(data, params);
|
||||
_onCreated.add(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
@override
|
||||
Future modify(id, Map data, [Map params]) async {
|
||||
var modified = await inner.modify(id, data, params);
|
||||
_onUpdated.add(modified);
|
||||
return modified;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future update(id, Map data, [Map params]) async {
|
||||
var updated = await inner.update(id, data, params);
|
||||
_onUpdated.add(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@override
|
||||
Future remove(id, [Map params]) async {
|
||||
var removed = await inner.remove(id, params);
|
||||
_onRemoved.add(removed);
|
||||
return removed;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
name: angel_framework
|
||||
version: 0.0.0-dev.12
|
||||
version: 0.0.0-dev.13
|
||||
description: Core libraries for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_framework
|
||||
|
|
|
@ -4,6 +4,11 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:json_god/json_god.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@Middleware(const ['interceptor'])
|
||||
testMiddlewareMetadata(RequestContext req, ResponseContext res) async {
|
||||
return "This should not be shown.";
|
||||
}
|
||||
|
||||
main() {
|
||||
group('routing', () {
|
||||
Angel angel;
|
||||
|
@ -26,6 +31,7 @@ main() {
|
|||
|
||||
todos.get('/action/:action', (req, res) => res.json(req.params));
|
||||
nested.post('/ted/:route', (req, res) => res.json(req.params));
|
||||
angel.get('/meta', testMiddlewareMetadata);
|
||||
angel.get('/intercepted', 'This should not be shown',
|
||||
middleware: ['interceptor']);
|
||||
angel.get('/hello', 'world');
|
||||
|
@ -33,6 +39,10 @@ main() {
|
|||
angel.post('/lambda', (req, res) => req.body);
|
||||
angel.use('/nes', nested);
|
||||
angel.use('/todos/:id', todos);
|
||||
angel.get('/greet/:name', (RequestContext req, res) async => "Hello ${req.params['name']}").as('Named routes');
|
||||
angel.get('/named', (req, ResponseContext res) async {
|
||||
res.redirectTo('Named routes', {'name': 'tests'});
|
||||
});
|
||||
angel.get('*', 'MJ');
|
||||
|
||||
client = new http.Client();
|
||||
|
@ -81,6 +91,12 @@ main() {
|
|||
expect(response.body, equals('Middleware'));
|
||||
});
|
||||
|
||||
test('Middleware via metadata', () async {
|
||||
// Metadata
|
||||
var response = await client.get('$url/meta');
|
||||
expect(response.body, equals('Middleware'));
|
||||
});
|
||||
|
||||
test('Can serialize function result as JSON', () async {
|
||||
Map headers = {'Content-Type': 'application/json'};
|
||||
String postData = god.serialize({'it': 'works'});
|
||||
|
@ -93,5 +109,18 @@ main() {
|
|||
var response = await client.get('$url/my_favorite_artist');
|
||||
expect(response.body, equals('"MJ"'));
|
||||
});
|
||||
|
||||
test('Can name routes', () {
|
||||
Route foo = angel.get('/framework/:id', 'Angel').as('frm');
|
||||
String uri = foo.makeUri({'id': 'angel'});
|
||||
print(uri);
|
||||
expect(uri, equals('/framework/angel'));
|
||||
});
|
||||
|
||||
test('Redirect to named routes', () async {
|
||||
var response = await client.get('$url/named');
|
||||
print(response.body);
|
||||
expect(god.deserialize(response.body), equals('Hello tests'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:mirrors';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:json_god/json_god.dart';
|
||||
|
@ -23,7 +24,8 @@ main() {
|
|||
angel = new Angel();
|
||||
client = new http.Client();
|
||||
god = new God();
|
||||
angel.use('/todos', new MemoryService<Todo>());
|
||||
Service todos = new MemoryService<Todo>();
|
||||
angel.use('/todos', todos);
|
||||
await angel.startServer(null, 0);
|
||||
url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}";
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ main() {
|
|||
});
|
||||
|
||||
test('can use app.properties like members', () {
|
||||
/*
|
||||
angel.properties['hello'] = 'world';
|
||||
angel.properties['foo'] = () => 'bar';
|
||||
angel.properties['Foo'] = new Foo('bar');
|
||||
|
@ -27,6 +28,7 @@ main() {
|
|||
expect(angel.hello, equals('world'));
|
||||
expect(angel.foo(), equals('bar'));
|
||||
expect(angel.Foo.name, equals('bar'));
|
||||
*/
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue