Route API change is breaking, haha.

This commit is contained in:
thosakwe 2016-10-22 16:41:36 -04:00
parent 1bb077a3d9
commit 551a7f086f
14 changed files with 447 additions and 571 deletions

View file

@ -1,28 +1,3 @@
library angel_framework.extensible;
import 'dart:mirrors';
/// Supports accessing members of a Map as though they were actual members.
class Extensible {
/// A set of custom properties that can be assigned to the server.
///
/// Useful for configuration and extension.
Map properties = {};
noSuchMethod(Invocation invocation) {
if (invocation.memberName != null) {
String name = MirrorSystem.getName(invocation.memberName);
if (properties.containsKey(name)) {
if (invocation.isGetter)
return properties[name];
else if (invocation.isMethod) {
return Function.apply(
properties[name], invocation.positionalArguments,
invocation.namedArguments);
}
}
}
super.noSuchMethod(invocation);
}
}
export 'package:angel_route/src/extensible.dart';

View file

@ -8,7 +8,10 @@ import 'routable.dart';
typedef Future<String> ViewGenerator(String path, [Map data]);
class AngelBase extends Routable {
AngelBase({bool debug: false}):super(debug: debug);
Container _container = new Container();
/// A [Container] used to inject dependencies.
Container get container => _container;

View file

@ -2,106 +2,20 @@ library angel_framework.http.controller;
import 'dart:async';
import 'dart:mirrors';
import 'package:angel_route/angel_route.dart';
import 'angel_base.dart';
import 'angel_http_exception.dart';
import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
import 'route.dart';
class Controller {
AngelBase app;
List middleware = [];
List<Route> routes = [];
Map<String, Route> routeMappings = {};
Expose exposeDecl;
Controller() {
// Load global expose decl
ClassMirror classMirror = reflectClass(this.runtimeType);
for (InstanceMirror metadatum in classMirror.metadata) {
if (metadatum.reflectee is Expose) {
exposeDecl = metadatum.reflectee;
break;
}
}
if (exposeDecl == null)
throw new Exception(
"All controllers must carry an @Expose() declaration.");
else routes.add(
new Route(
"*", "*", []..addAll(exposeDecl.middleware)..addAll(middleware)));
InstanceMirror instanceMirror = reflect(this);
classMirror.instanceMembers.forEach((Symbol key,
MethodMirror methodMirror) {
if (methodMirror.isRegularMethod && key != #toString &&
key != #noSuchMethod && key != #call && key != #equals &&
key != #==) {
InstanceMirror exposeMirror = methodMirror.metadata.firstWhere((
mirror) => mirror.reflectee is Expose, orElse: () => null);
if (exposeMirror != null) {
RequestHandler handler = (RequestContext req,
ResponseContext res) async {
List args = [];
// Load parameters, and execute
for (int i = 0; i < methodMirror.parameters.length; i++) {
ParameterMirror parameter = methodMirror.parameters[i];
if (parameter.type.reflectedType == RequestContext)
args.add(req);
else if (parameter.type.reflectedType == ResponseContext)
args.add(res);
else {String name = MirrorSystem.getName(parameter.simpleName);
var arg = req.params[name];
if (arg == null) {
if (parameter.type.reflectedType != dynamic) {
try {
arg = app.container.make(parameter.type.reflectedType);
if (arg != null) {
args.add(arg);
continue;
}
} catch(e) {
//
}
}
if (!exposeMirror.reflectee.allowNull.contain(name))
throw new AngelHttpException.BadRequest(message: "Missing parameter '$name'");
} else args.add(arg);
}
}
return await instanceMirror
.invoke(key, args)
.reflectee;
};
Route route = new Route(
exposeMirror.reflectee.method,
exposeMirror.reflectee.path,
[]
..addAll(exposeMirror.reflectee.middleware)
..add(handler));
routes.add(route);
String name = exposeMirror.reflectee.as;
if (name == null || name.isEmpty)
name = MirrorSystem.getName(key);
routeMappings[name] = route;
}
}
});
}
Future call(AngelBase app) async {
this.app = app;
app.use(exposeDecl.path, generateRoutable());
@ -115,5 +29,94 @@ class Controller {
app.controllers[name] = this;
}
Routable generateRoutable() => new Routable()..routes.addAll(routes);
Routable generateRoutable() {
final routable = new Routable();
// Load global expose decl
ClassMirror classMirror = reflectClass(this.runtimeType);
for (InstanceMirror metadatum in classMirror.metadata) {
if (metadatum.reflectee is Expose) {
exposeDecl = metadatum.reflectee;
break;
}
}
if (exposeDecl == null) {
throw new Exception(
"All controllers must carry an @Expose() declaration.");
}
final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware);
InstanceMirror instanceMirror = reflect(this);
classMirror.instanceMembers
.forEach((Symbol key, MethodMirror methodMirror) {
if (methodMirror.isRegularMethod &&
key != #toString &&
key != #noSuchMethod &&
key != #call &&
key != #equals &&
key != #==) {
InstanceMirror exposeMirror = methodMirror.metadata.firstWhere(
(mirror) => mirror.reflectee is Expose,
orElse: () => null);
if (exposeMirror != null) {
RequestHandler handler =
(RequestContext req, ResponseContext res) async {
List args = [];
// Load parameters, and execute
for (int i = 0; i < methodMirror.parameters.length; i++) {
ParameterMirror parameter = methodMirror.parameters[i];
if (parameter.type.reflectedType == RequestContext)
args.add(req);
else if (parameter.type.reflectedType == ResponseContext)
args.add(res);
else {
String name = MirrorSystem.getName(parameter.simpleName);
var arg = req.params[name];
if (arg == null) {
if (parameter.type.reflectedType != dynamic) {
try {
arg = app.container.make(parameter.type.reflectedType);
if (arg != null) {
args.add(arg);
continue;
}
} catch (e) {
//
}
}
if (!exposeMirror.reflectee.allowNull.contain(name))
throw new AngelHttpException.BadRequest(
message: "Missing parameter '$name'");
} else
args.add(arg);
}
}
return await instanceMirror.invoke(key, args).reflectee;
};
final route = routable.addRoute(exposeMirror.reflectee.method,
exposeMirror.reflectee.path, handler,
middleware: []
..addAll(handlers)
..addAll(exposeMirror.reflectee.middleware));
String name = exposeMirror.reflectee.as;
if (name == null || name.isEmpty) name = MirrorSystem.getName(key);
routeMappings[name] = route;
}
}
});
return routable;
}
}

View file

@ -4,7 +4,6 @@ import 'dart:async';
import 'package:merge_map/merge_map.dart';
import '../util.dart';
import 'metadata.dart';
import 'route.dart';
import 'service.dart';
/// Wraps another service in a service that broadcasts events on actions.
@ -13,84 +12,95 @@ class HookedService extends Service {
final Service inner;
HookedServiceEventDispatcher beforeIndexed =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeRead = new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeCreated =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeModified =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeUpdated =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher beforeRemoved =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterIndexed =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterRead = new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterCreated =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterModified =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterUpdated =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedServiceEventDispatcher afterRemoved =
new HookedServiceEventDispatcher();
new HookedServiceEventDispatcher();
HookedService(Service this.inner) {
// Clone app instance
if (inner.app != null)
this.app = inner.app;
if (inner.app != null) this.app = inner.app;
routes.clear();
// Set up our routes. We still need to copy middleware from inner service
Map restProvider = {'provider': Providers.REST};
// Add global middleware if declared on the instance itself
Middleware before = getAnnotation(inner, Middleware);
if (before != null) {
routes.add(new Route("*", "*", before.handlers));
}
final handlers = [];
if (before != null) handlers.add(before.handlers);
Middleware indexMiddleware = getAnnotation(inner.index, Middleware);
get('/', (req, res) async {
return await this.index(mergeMap([req.query, restProvider]));
}, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers);
},
middleware: []
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(inner.create, Middleware);
post('/', (req, res) async => await this.create(req.body, restProvider),
middleware:
(createMiddleware == null) ? [] : createMiddleware.handlers);
middleware: []
..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(inner.read, Middleware);
get(
'/:id',
(req, res) async => await this
.read(req.params['id'], mergeMap([req.query, restProvider])),
middleware: (readMiddleware == null) ? [] : readMiddleware.handlers);
.read(req.params['id'], mergeMap([req.query, restProvider])),
middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(inner.modify, Middleware);
patch(
'/:id',
(req, res) async =>
await this.modify(req.params['id'], req.body, restProvider),
middleware:
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers);
await this.modify(req.params['id'], req.body, restProvider),
middleware: []
..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(inner.update, Middleware);
post(
'/:id',
(req, res) async =>
await this.update(req.params['id'], req.body, restProvider),
middleware:
(updateMiddleware == null) ? [] : updateMiddleware.handlers);
await this.update(req.params['id'], req.body, restProvider),
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(inner.remove, Middleware);
delete(
'/:id',
(req, res) async => await this
.remove(req.params['id'], mergeMap([req.query, restProvider])),
middleware:
(removeMiddleware == null) ? [] : removeMiddleware.handlers);
.remove(req.params['id'], mergeMap([req.query, restProvider])),
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
}
@override

View file

@ -1,6 +1,7 @@
/// Various libraries useful for creating highly-extensible servers.
library angel_framework.http;
export 'package:angel_route/angel_route.dart';
export 'angel_base.dart';
export 'angel_http_exception.dart';
export 'base_middleware.dart';
@ -12,7 +13,6 @@ export 'memory_service.dart';
export 'request_context.dart';
export 'response_context.dart';
export 'routable.dart';
export 'route.dart';
export 'server.dart';
export 'service.dart';

View file

@ -1,13 +1,18 @@
library angel_framework.http.request_context;
import 'dart:async';
import 'dart:io';
import 'package:angel_route/src/extensible.dart';
import 'package:body_parser/body_parser.dart';
import '../../src/extensible.dart';
import 'angel_base.dart';
import 'route.dart';
/// A convenience wrapper around an incoming HTTP request.
class RequestContext extends Extensible {
BodyParseResult _body;
ContentType _contentType;
String _path;
HttpRequest _underlyingRequest;
/// The [Angel] instance that is responding to this request.
AngelBase app;
@ -27,59 +32,58 @@ class RequestContext extends Extensible {
String get method => underlyingRequest.method;
/// All post data submitted to the server.
Map body = {};
Map get body => _body.body;
/// The content type of an incoming request.
ContentType contentType;
ContentType get contentType => _contentType;
/// Any and all files sent to the server with this request.
List<FileUploadInfo> files = [];
List<FileUploadInfo> get files => _body.files;
/// The URL parameters extracted from the request URI.
Map params = {};
/// The requested path.
String path;
String get path => _path;
/// The parsed request query string.
Map query = {};
Map get query => _body.query;
/// The remote address requesting this resource.
InternetAddress remoteAddress;
/// The route that matched this request.
Route route;
InternetAddress get remoteAddress =>
underlyingRequest.connectionInfo.remoteAddress;
/// The user's HTTP session.
HttpSession session;
HttpSession get session => underlyingRequest.session;
/// The [Uri] instance representing the path this request is responding to.
Uri get uri => underlyingRequest.uri;
/// Is this an **XMLHttpRequest**?
bool get xhr => underlyingRequest.headers.value("X-Requested-With")
?.trim()
?.toLowerCase() == 'xmlhttprequest';
bool get xhr =>
underlyingRequest.headers
.value("X-Requested-With")
?.trim()
?.toLowerCase() ==
'xmlhttprequest';
/// The underlying [HttpRequest] instance underneath this context.
HttpRequest underlyingRequest;
HttpRequest get underlyingRequest => _underlyingRequest;
/// Magically transforms an [HttpRequest] into a RequestContext.
static Future<RequestContext> from(HttpRequest request,
Map parameters, AngelBase app, Route sourceRoute) async {
RequestContext context = new RequestContext();
/// Magically transforms an [HttpRequest] into a [RequestContext].
static Future<RequestContext> from(HttpRequest request, AngelBase app) async {
RequestContext ctx = new RequestContext();
context.app = app;
context.contentType = request.headers.contentType;
context.remoteAddress = request.connectionInfo.remoteAddress;
context.params = parameters;
context.path = request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), '');
context.route = sourceRoute;
context.session = request.session;
context.underlyingRequest = request;
ctx.app = app;
ctx._contentType = request.headers.contentType;
ctx._path = request.uri
.toString()
.replaceAll("?" + request.uri.query, "")
.replaceAll(new RegExp(r'/+$'), '');
ctx._underlyingRequest = request;
BodyParseResult bodyParseResult = await parseBody(request);
context.query = bodyParseResult.query;
context.body = bodyParseResult.body;
context.files = bodyParseResult.files;
ctx._body = await parseBody(request);
return context;
return ctx;
}
}

View file

@ -3,33 +3,35 @@ library angel_framework.http.response_context;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart';
import '../extensible.dart';
import 'angel_base.dart';
import 'controller.dart';
import 'route.dart';
/// A convenience wrapper around an outgoing HTTP request.
class ResponseContext extends Extensible {
bool _isOpen = true;
/// The [Angel] instance that is sending a response.
AngelBase app;
/// Can we still write to this response?
bool isOpen = true;
bool get isOpen => _isOpen;
/// A set of UTF-8 encoded bytes that will be written to the response.
List<List<int>> responseData = [];
final BytesBuilder buffer = new BytesBuilder();
/// Sets the status code to be sent with this response.
status(int code) {
void status(int code) {
underlyingResponse.statusCode = code;
}
/// The underlying [HttpResponse] under this instance.
HttpResponse underlyingResponse;
final HttpResponse underlyingResponse;
ResponseContext(this.underlyingResponse);
ResponseContext(this.underlyingResponse, this.app);
/// Any and all cookies to be sent to the user.
List<Cookie> get cookies => underlyingResponse.cookies;
@ -38,31 +40,37 @@ class ResponseContext extends Extensible {
bool willCloseItself = false;
/// Sends a download as a response.
download(File file, {String filename}) {
header("Content-Disposition", 'attachment; filename="${filename ?? file.path}"');
download(File file, {String filename}) async {
header("Content-Disposition",
'attachment; filename="${filename ?? file.path}"');
header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path));
header(HttpHeaders.CONTENT_LENGTH, file.lengthSync().toString());
responseData.add(file.readAsBytesSync());
buffer.add(await file.readAsBytes());
end();
}
/// Prevents more data from being written to the response.
end() => isOpen = false;
void end() {
_isOpen = false;
}
/// Sets a response header to the given value, or retrieves its value.
header(String key, [String value]) {
if (value == null) return underlyingResponse.headers[key];
else underlyingResponse.headers.set(key, value);
if (value == null)
return underlyingResponse.headers[key];
else
underlyingResponse.headers.set(key, value);
}
/// Serializes JSON to the response.
json(value) {
void json(value) {
write(god.serialize(value));
header(HttpHeaders.CONTENT_TYPE, ContentType.JSON.toString());
end();
}
/// Returns a JSONP response.
jsonp(value, {String callbackName: "callback"}) {
void jsonp(value, {String callbackName: "callback"}) {
write("$callbackName(${god.serialize(value)})");
header(HttpHeaders.CONTENT_TYPE, "application/javascript");
end();
@ -76,7 +84,7 @@ class ResponseContext extends Extensible {
}
/// Redirects to user to the given URL.
redirect(String url, {int code: 301}) {
void redirect(String url, {int code: 301}) {
header(HttpHeaders.LOCATION, url);
status(code ?? 301);
write('''
@ -100,22 +108,26 @@ class ResponseContext extends Extensible {
}
/// Redirects to the given named [Route].
redirectTo(String name, [Map params, int code]) {
void redirectTo(String name, [Map params, int code]) {
// Todo: Need to recurse route hierarchy, but also efficiently :)
Route matched = app.routes.firstWhere((Route route) => route.name == name);
if (matched != null) {
return redirect(matched.makeUri(params), code: code);
redirect(matched.makeUri(params), code: code);
return;
}
throw new ArgumentError.notNull('Route to redirect to ($name)');
}
/// Redirects to the given [Controller] action.
redirectToAction(String action, [Map params, int code]) {
void redirectToAction(String action, [Map params, int code]) {
// UserController@show
List<String> split = action.split("@");
// Todo: AngelResponseException
if (split.length < 2)
throw new Exception("Controller redirects must take the form of 'Controller@action'. You gave: $action");
throw new Exception(
"Controller redirects must take the form of 'Controller@action'. You gave: $action");
Controller controller = app.controller(split[0]);
@ -125,38 +137,30 @@ class ResponseContext extends Extensible {
Route matched = controller.routeMappings[split[1]];
if (matched == null)
throw new Exception("Controller '${split[0]}' does not contain any action named '${split[1]}'");
throw new Exception(
"Controller '${split[0]}' does not contain any action named '${split[1]}'");
return redirect(matched.makeUri(params), code: code);
redirect(matched.makeUri(params), code: code);
}
/// Streams a file to this response as chunked data.
///
/// Useful for video sites.
streamFile(File file,
Future streamFile(File file,
{int chunkSize, int sleepMs: 0, bool resumable: true}) async {
if (!isOpen) return;
header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path));
willCloseItself = true;
await file.openRead().pipe(underlyingResponse);
/*await chunked(file.openRead(), chunkSize: chunkSize,
sleepMs: sleepMs,
resumable: resumable);*/
}
/// Writes data to the response.
write(value) {
if (isOpen)
responseData.add(UTF8.encode(value.toString()));
}
/// Magically transforms an [HttpResponse] object into a ResponseContext.
static Future<ResponseContext> from
(HttpResponse response, AngelBase app) async
{
ResponseContext context = new ResponseContext(response);
context.app = app;
return context;
void write(value, {Encoding encoding: UTF8}) {
if (isOpen) {
if (value is List<int>)
buffer.add(value);
else buffer.add(encoding.encode(value.toString()));
}
}
}

View file

@ -2,8 +2,7 @@ library angel_framework.http.routable;
import 'dart:async';
import 'dart:io';
import 'dart:mirrors';
import '../extensible.dart';
import 'package:angel_route/angel_route.dart';
import '../util.dart';
import 'angel_base.dart';
import 'controller.dart';
@ -11,11 +10,8 @@ import 'hooked_service.dart';
import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'route.dart';
import 'service.dart';
typedef Route RouteAssigner(Pattern path, handler, {List middleware});
/// A function that intercepts a request and determines whether handling of it should continue.
typedef Future<bool> RequestMiddleware(RequestContext req, ResponseContext res);
@ -26,20 +22,26 @@ typedef Future RequestHandler(RequestContext req, ResponseContext res);
typedef Future RawRequestHandler(HttpRequest request);
/// A routable server that can handle dynamic requests.
class Routable extends Extensible {
/// Additional filters to be run on designated requests.
Map <String, RequestMiddleware> requestMiddleware = {};
class Routable extends Router {
final Map<String, Controller> _controllers = {};
final Map<Pattern, Service> _services = {};
/// Dynamic request paths that this server will respond to.
List<Route> routes = [];
Routable({bool debug: false}) : super(debug: debug);
/// Additional filters to be run on designated requests.
@override
final Map<String, RequestMiddleware> requestMiddleware = {};
/// A set of [Service] objects that have been mapped into routes.
Map <Pattern, Service> services = {};
Map<Pattern, Service> get services =>
new Map<Pattern, Service>.unmodifiable(_services);
/// A set of [Controller] objects that have been loaded into the application.
Map<String, Controller> controllers = {};
Map<String, Controller> get controllers =>
new Map<String, Controller>.unmodifiable(_controllers);
StreamController<Service> _onService = new StreamController<Service>.broadcast();
StreamController<Service> _onService =
new StreamController<Service>.broadcast();
/// Fired whenever a service is added to this instance.
///
@ -47,133 +49,80 @@ class Routable extends Extensible {
Stream<Service> get onService => _onService.stream;
/// Assigns a middleware to a name for convenience.
registerMiddleware(String name, RequestMiddleware middleware) {
this.requestMiddleware[name] = middleware;
}
@override
registerMiddleware(String name, RequestMiddleware middleware) =>
super.registerMiddleware(name, middleware);
/// Retrieves the service assigned to the given path.
Service service(Pattern path) => services[path];
Service service(Pattern path) => _services[path];
/// Retrieves the controller with the given name.
Controller controller(String name) => controllers[name];
/// 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.
void use(Pattern path, Routable routable,
{bool hooked: true, String middlewareNamespace: null}) {
Routable _routable = routable;
// If we need to hook this service, do it here. It has to be first, or
// else all routes will point to the old service.
if (_routable is Service) {
Hooked hookedDeclaration = getAnnotation(_routable, Hooked);
Service service = (hookedDeclaration != null || hooked)
? new HookedService(_routable)
: _routable;
services[path.toString().trim().replaceAll(
new RegExp(r'(^\/+)|(\/+$)'), '')] = service;
_routable = service;
}
if (_routable is AngelBase) {
all(path, (RequestContext req, ResponseContext res) async {
req.app = _routable;
res.app = _routable;
return true;
});
}
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'\$$'), '')));
route.path = "$path${route.path}";
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)
_onService.add(routable);
}
/// 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.
@override
Route addRoute(String method, Pattern path, Object handler,
{List middleware}) {
List handlers = [];
final List handlers = [];
// Merge @Middleware declaration, if any
Middleware middlewareDeclaration = getAnnotation(
handler, Middleware);
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;
return super.addRoute(method, path, handler,
middleware: []..addAll(middleware ?? [])..addAll(handlers));
}
/// Adds a route that responds to any request matching the given path.
Route all(Pattern path, Object handler, {List middleware}) {
return addRoute('*', path, handler, middleware: middleware);
}
void use(Pattern path, Router router,
{bool hooked: true, String namespace: null}) {
Router _router = router;
Service service;
/// Adds a route that responds to a GET request.
Route get(Pattern path, Object handler, {List middleware}) {
return addRoute('GET', path, handler, middleware: middleware);
}
// If we need to hook this service, do it here. It has to be first, or
// else all routes will point to the old service.
if (router is Service) {
Hooked hookedDeclaration = getAnnotation(router, Hooked);
_router = service = (hookedDeclaration != null || hooked)
? new HookedService(router)
: router;
_services[path
.toString()
.trim()
.replaceAll(new RegExp(r'(^/+)|(/+$)'), '')] = service;
}
/// Adds a route that responds to a POST request.
Route post(Pattern path, Object handler, {List middleware}) {
return addRoute('POST', path, handler, middleware: middleware);
}
final handlers = [];
if (_router is AngelBase) {
handlers.add((RequestContext req, ResponseContext res) async {
req.app = _router;
res.app = _router;
return true;
});
}
/// Adds a route that responds to a PATCH request.
Route patch(Pattern path, Object handler, {List middleware}) {
return addRoute('PATCH', path, handler, middleware: middleware);
}
// Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = namespace != null ? "$namespace." : "";
/// Adds a route that responds to a DELETE request.
Route delete(Pattern path, Object handler, {List middleware}) {
return addRoute('DELETE', path, handler, middleware: middleware);
}
Map copiedMiddleware = new Map.from(router.requestMiddleware);
for (String middlewareName in copiedMiddleware.keys) {
requestMiddleware["$middlewarePrefix$middlewareName"] =
copiedMiddleware[middlewareName];
}
Routable() {
}
root.child(path, debug: debug, handlers: handlers).addChild(router.root);
_router.dumpTree(header: 'Mounting on "$path":');
if (router is Routable) {
// Copy services, too. :)
for (Pattern servicePath in _router._services.keys) {
String newServicePath =
path.toString().trim().replaceAll(new RegExp(r'(^/+)|(/+$)'), '') +
'/$servicePath';
_services[newServicePath] = _router._services[servicePath];
}
}
if (service != null) _onService.add(service);
}
}

View file

@ -1,91 +0,0 @@
library angel_framework.http.route;
/// Represents an endpoint open for connection via the Internet.
class Route {
/// A regular expression used to match URI's to this route.
RegExp matcher;
/// The HTTP method this route responds to.
String method;
/// An array of functions, Futures and objects that can respond to this route.
List handlers = [];
/// The path this route is mounted on.
String path;
/// (Optional) - A name for this route.
String name;
Route(String method, Pattern path, [List handlers]) {
this.method = method;
if (path is RegExp) {
this.matcher = path;
this.path = path.pattern;
}
else {
this.matcher = new RegExp('^' +
path.toString()
.replaceAll(new RegExp(r'\/\*$'), "*")
.replaceAll(new RegExp('\/'), r'\/')
.replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)')
.replaceAll(new RegExp('\\*'), '.*')
+ r'$');
this.path = path;
}
if (handlers != null) {
this.handlers.addAll(handlers);
}
}
/// Assigns a name to this Route.
as(String name) {
this.name = name;
return this;
}
/// Generates a URI to this route with the given parameters.
String makeUri([Map<String, dynamic> params]) {
String result = path;
if (params != null) {
for (String key in (params.keys)) {
result = result.replaceAll(new RegExp(":$key" + r"\??"), params[key].toString());
}
}
return result.replaceAll("*", "");
}
/// Enables one or more handlers to be called whenever this route is visited.
Route middleware(handler) {
if (handler is Iterable)
handlers.addAll(handler);
else handlers.add(handler);
return this;
}
/// Extracts route parameters from a given path.
Map parseParameters(String requestPath) {
Map result = {};
Iterable<String> values = _parseParameters(requestPath);
RegExp rgx = new RegExp(':([a-zA-Z_]+)');
Iterable<Match> matches = rgx.allMatches(
path.replaceAll(new RegExp('\/'), r'\/'));
for (int i = 0; i < matches.length; i++) {
Match match = matches.elementAt(i);
String paramName = match.group(1);
String value = values.elementAt(i);
num numValue = num.parse(value, (_) => double.NAN);
if (!numValue.isNaN)
result[paramName] = numValue;
else
result[paramName] = value;
}
return result;
}
_parseParameters(String requestPath) sync* {
Match routeMatch = matcher.firstMatch(requestPath);
for (int i = 1; i <= routeMatch.groupCount; i++)
yield routeMatch.group(i);
}
}

View file

@ -11,10 +11,11 @@ import 'controller.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
import 'route.dart';
import 'service.dart';
export 'package:container/container.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// A function that binds an [Angel] server to an Internet address and port.
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
@ -31,6 +32,7 @@ class Angel extends AngelBase {
var _beforeProcessed = new StreamController<HttpRequest>.broadcast();
var _fatalErrorStream = new StreamController<Map>.broadcast();
var _onController = new StreamController<Controller>.broadcast();
final Random _rand = new Random.secure();
ServerGenerator _serverGenerator =
(address, port) async => await HttpServer.bind(address, port);
@ -74,6 +76,23 @@ class Angel extends AngelBase {
/// The native HttpServer running this instancce.
HttpServer httpServer;
/// Handles a server error.
_onError(e, [StackTrace stackTrace]) {
_fatalErrorStream.add({"error": e, "stack": stackTrace});
}
void _printDebug(x) {
if (debug) print(x);
}
String _randomString(int length) {
var codeUnits = new List.generate(length, (index) {
return _rand.nextInt(33) + 89;
});
return new String.fromCharCodes(codeUnits);
}
/// Starts the server.
///
/// Returns false on failure; otherwise, returns the HttpServer.
@ -81,10 +100,7 @@ class Angel extends AngelBase {
var server = await _serverGenerator(
address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0);
this.httpServer = server;
server.listen(handleRequest);
return server;
return server..listen(handleRequest);
}
/// Loads some base dependencies into the service container.
@ -95,75 +111,7 @@ class Angel extends AngelBase {
if (runtimeType != Angel) container.singleton(this, as: Angel);
}
Future handleRequest(HttpRequest request) async {
_beforeProcessed.add(request);
String requestedUrl = request.uri
.toString()
.replaceAll("?" + request.uri.query, "")
.replaceAll(new RegExp(r'\/+$'), '');
if (requestedUrl.isEmpty) requestedUrl = '/';
RequestContext req = await RequestContext.from(request, {}, this, null);
ResponseContext res = await ResponseContext.from(request.response, this);
bool canContinue = true;
executeHandler(handler, req) async {
if (canContinue) {
try {
canContinue = await _applyHandler(handler, req, res);
} catch (e, stackTrace) {
if (e is AngelHttpException) {
// Special handling for AngelHttpExceptions :)
try {
res.status(e.statusCode);
String accept = request.headers.value(HttpHeaders.ACCEPT);
if (accept == "*/*" ||
accept.contains(ContentType.JSON.mimeType) ||
accept.contains("application/javascript")) {
res.json(e.toMap());
} else {
await _errorHandler(e, req, res);
}
_finalizeResponse(request, res);
} catch (_) {}
}
_onError(e, stackTrace);
canContinue = false;
return false;
}
} else
return false;
}
for (var handler in before) {
await executeHandler(handler, req);
}
for (Route route in routes) {
if (!canContinue) break;
if (route.matcher.hasMatch(requestedUrl) &&
(request.method == route.method || route.method == '*')) {
req.params = route.parseParameters(requestedUrl);
req.route = route;
for (var handler in route.handlers) {
await executeHandler(handler, req);
}
}
}
for (var handler in after) {
await executeHandler(handler, req);
}
_finalizeResponse(request, res);
}
Future<bool> _applyHandler(
Future<bool> executeHandler(
handler, RequestContext req, ResponseContext res) async {
if (handler is RequestMiddleware) {
var result = await handler(req, res);
@ -216,7 +164,7 @@ class Angel extends AngelBase {
}
if (requestMiddleware.containsKey(handler)) {
return await _applyHandler(requestMiddleware[handler], req, res);
return await executeHandler(requestMiddleware[handler], req, res);
}
res.willCloseItself = true;
@ -225,11 +173,76 @@ class Angel extends AngelBase {
return false;
}
_finalizeResponse(HttpRequest request, ResponseContext res) async {
Future handleRequest(HttpRequest request) async {
_beforeProcessed.add(request);
final req = await RequestContext.from(request, this);
final res = new ResponseContext(request.response, this);
String requestedUrl = request.uri
.toString()
.replaceAll("?" + request.uri.query, "")
.replaceAll(_straySlashes, '');
if (requestedUrl.isEmpty) requestedUrl = '/';
final route = resolve(requestedUrl,
(route) => route.method == request.method || route.method == '*');
print('Resolve ${requestedUrl} -> $route');
req.params.addAll(route?.parseParameters(requestedUrl) ?? {});
final handlerSequence = []..addAll(before);
if (route != null) handlerSequence.addAll(route.handlerSequence);
handlerSequence.addAll(after);
_printDebug('Handler sequence on $requestedUrl: $handlerSequence');
for (final handler in handlerSequence) {
try {
_printDebug('Executing handler: $handler');
final result = await executeHandler(handler, req, res);
_printDebug('Result: $result');
if (!result) {
_printDebug('Last executed handler: $handler');
break;
} else {
_printDebug(
'Handler completed successfully, did not terminate response: $handler');
}
} catch (e, st) {
_printDebug('Caught error in handler $handler: $e');
_printDebug(st);
if (e is AngelHttpException) {
// Special handling for AngelHttpExceptions :)
try {
res.status(e.statusCode);
String accept = request.headers.value(HttpHeaders.ACCEPT);
if (accept == "*/*" ||
accept.contains(ContentType.JSON.mimeType) ||
accept.contains("application/javascript")) {
res.json(e.toMap());
} else {
await _errorHandler(e, req, res);
}
_finalizeResponse(request, res);
} catch (_) {
// Todo: This exception needs to be caught as well.
}
} else {
// Todo: Uncaught exceptions need to be... Caught.
}
_onError(e, st);
break;
}
}
try {
_afterProcessed.add(request);
if (!res.willCloseItself) {
res.responseData.forEach((blob) => request.response.add(blob));
request.response.add(res.buffer.takeBytes());
await request.response.close();
}
} catch (e) {
@ -237,15 +250,6 @@ class Angel extends AngelBase {
}
}
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);
}
// Run a function after injecting from service container
Future runContained(Function handler, RequestContext req, ResponseContext res,
{Map<String, dynamic> namedParameters,
@ -302,12 +306,12 @@ class Angel extends AngelBase {
@override
use(Pattern path, Routable routable,
{bool hooked: true, String middlewareNamespace: null}) {
{bool hooked: true, String namespace: null}) {
if (routable is Service) {
routable.app = this;
}
return super.use(path, routable,
hooked: hooked, middlewareNamespace: middlewareNamespace);
return super.use(path, routable, hooked: hooked, namespace: namespace);
}
/// Registers a callback to run upon errors.
@ -315,15 +319,7 @@ class Angel extends AngelBase {
_errorHandler = handler;
}
/// Handles a server error.
_onError(e, [StackTrace stackTrace]) {
_fatalErrorStream.add({
"error": e,
"stack": stackTrace
});
}
Angel() : super() {
Angel({bool debug: false}) : super(debug: debug) {
bootstrapContainer();
}
@ -332,8 +328,8 @@ class Angel extends AngelBase {
/// If no password is provided, a random one will be generated upon running
/// the server.
Angel.secure(String certificateChainPath, String serverKeyPath,
{String password})
: super() {
{bool debug: false, String password})
: super(debug: debug) {
bootstrapContainer();
_serverGenerator = (InternetAddress address, int port) async {
var certificateChain =

View file

@ -2,13 +2,11 @@ library angel_framework.http.service;
import 'dart:async';
import 'package:merge_map/merge_map.dart';
import '../defs.dart';
import '../util.dart';
import 'angel_base.dart';
import 'angel_http_exception.dart';
import 'metadata.dart';
import 'routable.dart';
import 'route.dart';
/// Indicates how the service was accessed.
///
@ -72,19 +70,24 @@ class Service extends Routable {
// Add global middleware if declared on the instance itself
Middleware before = getAnnotation(this, Middleware);
if (before != null) {
routes.add(new Route("*", "*", before.handlers));
}
final handlers = [];
if (before != null) handlers.add(before.handlers);
Middleware indexMiddleware = getAnnotation(this.index, Middleware);
get('/', (req, res) async {
return await this.index(mergeMap([req.query, restProvider]));
}, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers);
},
middleware: []
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(this.create, Middleware);
post('/', (req, res) async => await this.create(req.body, restProvider),
middleware:
(createMiddleware == null) ? [] : createMiddleware.handlers);
middleware: []
..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(this.read, Middleware);
@ -92,30 +95,38 @@ class Service extends Routable {
'/:id',
(req, res) async => await this
.read(req.params['id'], mergeMap([req.query, restProvider])),
middleware: (readMiddleware == null) ? [] : readMiddleware.handlers);
middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(this.modify, Middleware);
patch(
'/:id',
(req, res) async =>
await this.modify(req.params['id'], req.body, restProvider),
middleware:
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers);
middleware: []
..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(this.update, Middleware);
post(
'/:id',
(req, res) async =>
await this.update(req.params['id'], req.body, restProvider),
middleware:
(updateMiddleware == null) ? [] : updateMiddleware.handlers);
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(this.remove, Middleware);
delete(
'/:id',
(req, res) async => await this
.remove(req.params['id'], mergeMap([req.query, restProvider])),
middleware:
(removeMiddleware == null) ? [] : removeMiddleware.handlers);
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
}
}

View file

@ -4,6 +4,8 @@ description: Core libraries for the Angel framework.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_framework
dependencies:
angel_route:
path: ../angel_route
body_parser: ">=1.0.0-dev <2.0.0"
container: ">=0.1.2 <1.0.0"
json_god: ">=2.0.0-beta <3.0.0"

View file

@ -26,32 +26,43 @@ main() {
http.Client client;
setUp(() async {
angel = new Angel();
nested = new Angel();
todos = new Angel();
final debug = false;
angel = new Angel(debug: debug);
nested = new Angel(debug: debug);
todos = new Angel(debug: debug);
angel..registerMiddleware('interceptor', (req, res) async {
res.write('Middleware');
return false;
})..registerMiddleware('intercept_service',
(RequestContext req, res) async {
print("Intercepting a service!");
return true;
});
angel
..registerMiddleware('interceptor', (req, res) async {
res.write('Middleware');
return false;
})
..registerMiddleware('intercept_service',
(RequestContext req, res) async {
print("Intercepting a service!");
return true;
});
todos.get('/action/:action', (req, res) => res.json(req.params));
nested.post('/ted/:route', (req, res) => res.json(req.params));
Route ted;
ted = nested.post('/ted/:route', (RequestContext req, res) {
print('Params: ${req.params}');
print('Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}');
return req.params;
});
angel.use('/nes', nested);
angel.get('/meta', testMiddlewareMetadata);
angel.get('/intercepted', 'This should not be shown',
middleware: ['interceptor']);
angel.get('/hello', 'world');
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('/greet/:name',
(RequestContext req, res) async => "Hello ${req.params['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'});
@ -60,13 +71,11 @@ main() {
print("Query: ${req.query}");
return "Logged";
});
angel.use('/query', new QueryService());
angel.get('*', 'MJ');
print("DUMPING ROUTES: ");
for (Route route in angel.routes) {
print("${route.method} ${route.path} - ${route.handlers}");
}
angel.dumpTree(header: "DUMPING ROUTES:");
client = new http.Client();
await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
@ -90,6 +99,7 @@ main() {
test('Can match url with multiple parameters', () async {
var response = await client.get('$url/name/HELLO/last/WORLD');
print(response.body);
var json = god.deserialize(response.body);
expect(json['first'], equals('HELLO'));
expect(json['last'], equals('WORLD'));
@ -104,6 +114,7 @@ main() {
test('Can parse parameters from a nested Angel instance', () async {
var response = await client.get('$url/todos/1337/action/test');
var json = god.deserialize(response.body);
print('JSON: $json');
expect(json['id'], equals(1337));
expect(json['action'], equals('test'));
});
@ -123,7 +134,7 @@ main() {
Map headers = {'Content-Type': 'application/json'};
String postData = god.serialize({'it': 'works'});
var response =
await client.post("$url/lambda", headers: headers, body: postData);
await client.post("$url/lambda", headers: headers, body: postData);
expect(god.deserialize(response.body)['it'], equals('works'));
});
@ -133,10 +144,11 @@ main() {
});
test('Can name routes', () {
Route foo = angel.get('/framework/:id', 'Angel').as('frm');
Route foo = new Route('/framework/:id', name: 'frm');
print('Foo: $foo');
String uri = foo.makeUri({'id': 'angel'});
print(uri);
expect(uri, equals('/framework/angel'));
expect(uri, equals('framework/angel'));
});
test('Redirect to named routes', () async {
@ -147,7 +159,7 @@ main() {
test('Match routes, even with query params', () async {
var response =
await client.get("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo");
await client.get("$url/log?foo=bar&bar=baz&baz.foo=bar&baz.bar=foo");
print(response.body);
expect(god.deserialize(response.body), equals('Logged'));

View file

@ -24,11 +24,9 @@ main() {
angel.properties['foo'] = () => 'bar';
angel.properties['Foo'] = new Foo('bar');
/**
expect(angel.hello, equals('world'));
expect(angel.foo(), equals('bar'));
expect(angel.Foo.name, equals('bar'));
*/
});
});
}