@Middleware, @Hooked, .use-> services, middleware

This commit is contained in:
regiostech 2016-05-02 18:28:14 -04:00
parent 496ecd8757
commit f58b2dc259
13 changed files with 255 additions and 33 deletions

View file

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

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

View file

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

View file

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

View file

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

View file

@ -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 = {};

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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