From 551a7f086fb76da53a9fc5175fbe037445af206f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 22 Oct 2016 16:41:36 -0400 Subject: [PATCH] Route API change is breaking, haha. --- lib/src/extensible.dart | 27 +--- lib/src/http/angel_base.dart | 3 + lib/src/http/controller.dart | 181 +++++++++++++------------- lib/src/http/hooked_service.dart | 72 ++++++----- lib/src/http/http.dart | 2 +- lib/src/http/request_context.dart | 72 ++++++----- lib/src/http/response_context.dart | 80 ++++++------ lib/src/http/routable.dart | 193 +++++++++++----------------- lib/src/http/route.dart | 91 -------------- lib/src/http/server.dart | 196 ++++++++++++++--------------- lib/src/http/service.dart | 41 +++--- pubspec.yaml | 2 + test/routing.dart | 56 +++++---- test/util.dart | 2 - 14 files changed, 447 insertions(+), 571 deletions(-) delete mode 100644 lib/src/http/route.dart diff --git a/lib/src/extensible.dart b/lib/src/extensible.dart index b14b8255..45ad12d4 100644 --- a/lib/src/extensible.dart +++ b/lib/src/extensible.dart @@ -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); - } -} \ No newline at end of file +export 'package:angel_route/src/extensible.dart'; \ No newline at end of file diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart index 2229c02d..25fc84be 100644 --- a/lib/src/http/angel_base.dart +++ b/lib/src/http/angel_base.dart @@ -8,7 +8,10 @@ import 'routable.dart'; typedef Future 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; diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 08f94aa1..f48516c5 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -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 routes = []; Map 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); -} \ No newline at end of file + 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; + } +} diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart index 3fe18df4..546e2e52 100644 --- a/lib/src/http/hooked_service.dart +++ b/lib/src/http/hooked_service.dart @@ -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 diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 92eb2c4c..c2859a48 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -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'; diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index 3ae4d88a..af2544a3 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -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 files = []; + List 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 from(HttpRequest request, - Map parameters, AngelBase app, Route sourceRoute) async { - RequestContext context = new RequestContext(); + /// Magically transforms an [HttpRequest] into a [RequestContext]. + static Future 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; } -} \ No newline at end of file +} diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 5067b88c..84ffedb2 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -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> 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 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 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())); + void write(value, {Encoding encoding: UTF8}) { + if (isOpen) { + if (value is List) + buffer.add(value); + else buffer.add(encoding.encode(value.toString())); + } } - - /// Magically transforms an [HttpResponse] object into a ResponseContext. - static Future from - (HttpResponse response, AngelBase app) async - { - ResponseContext context = new ResponseContext(response); - context.app = app; - return context; - } -} \ No newline at end of file +} diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 4d65ce72..ae1d844d 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -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 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 requestMiddleware = {}; +class Routable extends Router { + final Map _controllers = {}; + final Map _services = {}; - /// Dynamic request paths that this server will respond to. - List routes = []; + Routable({bool debug: false}) : super(debug: debug); + + /// Additional filters to be run on designated requests. + @override + final Map requestMiddleware = {}; /// A set of [Service] objects that have been mapped into routes. - Map services = {}; + Map get services => + new Map.unmodifiable(_services); /// A set of [Controller] objects that have been loaded into the application. - Map controllers = {}; + Map get controllers => + new Map.unmodifiable(_controllers); - StreamController _onService = new StreamController.broadcast(); + StreamController _onService = + new StreamController.broadcast(); /// Fired whenever a service is added to this instance. /// @@ -47,133 +49,80 @@ class Routable extends Extensible { Stream 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); -} \ No newline at end of file + _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); + } +} diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart deleted file mode 100644 index ab3588de..00000000 --- a/lib/src/http/route.dart +++ /dev/null @@ -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 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 values = _parseParameters(requestPath); - RegExp rgx = new RegExp(':([a-zA-Z_]+)'); - Iterable 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); - } -} diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 197e982c..5a47d8a9 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -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 ServerGenerator(InternetAddress address, int port); @@ -31,6 +32,7 @@ class Angel extends AngelBase { var _beforeProcessed = new StreamController.broadcast(); var _fatalErrorStream = new StreamController.broadcast(); var _onController = new StreamController.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 _applyHandler( + Future 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 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 = diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index c12e8a35..b04ddd4a 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -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)); } } diff --git a/pubspec.yaml b/pubspec.yaml index abb87f81..b2431cab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,6 +4,8 @@ description: Core libraries for the Angel framework. author: Tobe O 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" diff --git a/test/routing.dart b/test/routing.dart index d72b1a90..6acb9a8f 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -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')); diff --git a/test/util.dart b/test/util.dart index 5255d8db..2d22fedd 100644 --- a/test/util.dart +++ b/test/util.dart @@ -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')); - */ }); }); }