From 31607686beab5cbacccf24573a9825b5dfd95278 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 28 Feb 2016 08:11:17 -0500 Subject: [PATCH 001/414] Bless up --- .gitignore | 65 ++++++++++++++++++++++++++++++++++++++ lib/angel_framework.dart | 4 +++ lib/src/http/http.dart | 11 +++++++ lib/src/http/routable.dart | 10 ++++++ lib/src/http/route.dart | 14 ++++++++ lib/src/http/server.dart | 39 +++++++++++++++++++++++ pubspec.yaml | 7 ++++ 7 files changed, 150 insertions(+) create mode 100644 .gitignore create mode 100644 lib/angel_framework.dart create mode 100644 lib/src/http/http.dart create mode 100644 lib/src/http/routable.dart create mode 100644 lib/src/http/route.dart create mode 100644 lib/src/http/server.dart create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..917abe7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +### Dart template +# Don’t commit the following directories created by pub. +.buildlog +.pub/ +build/ +packages +.packages + +# Or the files created by dart2js. +*.dart.js +*.js_ +*.js.deps +*.js.map + +# Include when developing application packages. +pubspec.lock + diff --git a/lib/angel_framework.dart b/lib/angel_framework.dart new file mode 100644 index 00000000..207d8ea8 --- /dev/null +++ b/lib/angel_framework.dart @@ -0,0 +1,4 @@ +/// A controller-based MVC + WebSocket framework in Dart. +library angel_framework; + +export 'src/http/http.dart'; \ No newline at end of file diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart new file mode 100644 index 00000000..3703c3bf --- /dev/null +++ b/lib/src/http/http.dart @@ -0,0 +1,11 @@ +/// HTTP logic +library angel_framework.http; + +import 'dart:async'; +import 'dart:io'; +import 'package:json_god/json_god.dart'; +import 'package:route/server.dart'; + +part 'route.dart'; +part 'routable.dart'; +part 'server.dart'; \ No newline at end of file diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart new file mode 100644 index 00000000..5f91102c --- /dev/null +++ b/lib/src/http/routable.dart @@ -0,0 +1,10 @@ +part of angel_framework.http; + +/// A routable server that can handle dynamic requests. +class Routable { + /// Additional filters to be run on designated requests. + Map middleware = {}; + + /// Dynamic request paths that this server will respond to. + Map routes = {}; +} \ No newline at end of file diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart new file mode 100644 index 00000000..669013c0 --- /dev/null +++ b/lib/src/http/route.dart @@ -0,0 +1,14 @@ +part of angel_framework.http; + +class Route { + Pattern matcher; + String method; + + Route(String method, Pattern path, [List handlers]) { + this.method = method; + if (path is RegExp) this.matcher = path; + else this.matcher = new RegExp('^' + + path.toString().replaceAll(new RegExp('\/'), r'\/').replaceAll( + new RegExp(':[a-zA-Z_]+'), '([^\/]+)') + r'$'); + } +} diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart new file mode 100644 index 00000000..c98ca182 --- /dev/null +++ b/lib/src/http/server.dart @@ -0,0 +1,39 @@ +part of angel_framework.http; + +/// A function that binds +typedef Future ServerGenerator(InternetAddress address, int port); + +/// A powerful real-time/REST/MVC server class. +class Angel extends Routable { + ServerGenerator _serverGenerator; + + _startServer(InternetAddress address, int port) async { + var server = await _serverGenerator( + address ?? InternetAddress.LOOPBACK_IP_V4, port); + var router = new Router(server); + + this.routes.forEach((Route route, value) { + router.serve(route.matcher, method: route.method).listen(( + HttpRequest request) { + + }); + }); + } + + /// Starts the server. + void listen({InternetAddress address, int port: 3000}) { + runZoned(() async { + await _startServer(address, port); + }, onError: onError); + } + + /// Handles a server error. + void onError(e, [StackTrace stackTrace]) { + + } + + Angel() {} + + /// Creates an HTTPS server. + Angel.secure() {} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..86b44c74 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,7 @@ +name: angel_framework +version: 0.0.1-dev +description: Core libraries for the Angel framework. +author: Tobe O +dependencies: + json_god: any + route: any \ No newline at end of file From 95369495e4099a29d4d74ce6f1ac89d1e459570a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 17 Apr 2016 23:27:23 -0400 Subject: [PATCH 002/414] Almost done. --- lib/src/http/http.dart | 10 ++- lib/src/http/request_context.dart | 88 ++++++++++++++++++++ lib/src/http/response_context.dart | 129 +++++++++++++++++++++++++++++ lib/src/http/routable.dart | 53 +++++++++++- lib/src/http/route.dart | 52 ++++++++++-- lib/src/http/server.dart | 119 +++++++++++++++++++++++--- lib/src/http/service.dart | 54 ++++++++++++ lib/src/http/services/memory.dart | 31 +++++++ pubspec.yaml | 7 +- test/routing.dart | 72 ++++++++++++++++ test/services.dart | 54 ++++++++++++ test/util.dart | 32 +++++++ 12 files changed, 681 insertions(+), 20 deletions(-) create mode 100644 lib/src/http/request_context.dart create mode 100644 lib/src/http/response_context.dart create mode 100644 lib/src/http/service.dart create mode 100644 lib/src/http/services/memory.dart create mode 100644 test/routing.dart create mode 100644 test/services.dart create mode 100644 test/util.dart diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 3703c3bf..dd0d4a70 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -2,10 +2,18 @@ library angel_framework.http; import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:mirrors'; +import 'package:body_parser/body_parser.dart'; import 'package:json_god/json_god.dart'; +import 'package:mime/mime.dart'; import 'package:route/server.dart'; +part 'request_context.dart'; +part 'response_context.dart'; part 'route.dart'; part 'routable.dart'; -part 'server.dart'; \ No newline at end of file +part 'server.dart'; +part 'service.dart'; +part 'services/memory.dart'; \ No newline at end of file diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart new file mode 100644 index 00000000..35bb7e23 --- /dev/null +++ b/lib/src/http/request_context.dart @@ -0,0 +1,88 @@ +part of angel_framework.http; + +/// A function that intercepts a request and determines whether handling of it should continue. +typedef Future Middleware(RequestContext req, ResponseContext res); + +/// A function that receives an incoming [RequestContext] and responds to it. +typedef Future RequestHandler(RequestContext req, ResponseContext res); + +/// A function that handles an [HttpRequest]. +typedef Future RawRequestHandler(HttpRequest request); + +/// A convenience wrapper around an incoming HTTP request. +class RequestContext { + /// The [Angel] instance that is responding to this request. + Angel app; + + /// Any cookies sent with this request. + List get cookies => underlyingRequest.cookies; + + /// All HTTP headers sent with this request. + HttpHeaders get headers => underlyingRequest.headers; + + /// The requested hostname. + String get hostname => underlyingRequest.headers.value(HttpHeaders.HOST); + + /// The user's IP. + String get ip => remoteAddress.address; + + /// This request's HTTP method. + String get method => underlyingRequest.method; + + /// All post data submitted to the server. + Map body = {}; + + /// The content type of an incoming request. + ContentType contentType; + + /// Any and all files sent to the server with this request. + List files = []; + + /// The URL parameters extracted from the request URI. + Map params = {}; + + /// The requested path. + String path; + + /// The parsed request query string. + Map query = {}; + + /// The remote address requesting this resource. + InternetAddress remoteAddress; + + /// The route that matched this request. + Route route; + + /// The user's HTTP session. + HttpSession session; + + /// Is this an **XMLHttpRequest**? + bool get xhr => underlyingRequest.headers.value("X-Requested-With") + ?.trim() + ?.toLowerCase() == 'xmlhttprequest'; + + /// The underlying [HttpRequest] instance underneath this context. + HttpRequest underlyingRequest; + + /// Magically transforms an [HttpRequest] into a RequestContext. + static Future from(HttpRequest request, + Map parameters, Angel app, Route sourceRoute) async { + RequestContext context = new RequestContext(); + + context.app = app; + context.contentType = request.headers.contentType; + context.remoteAddress = request.connectionInfo.remoteAddress; + context.params = parameters; + context.path = request.uri.toString(); + context.route = sourceRoute; + context.session = request.session; + context.underlyingRequest = request; + + BodyParseResult bodyParseResult = await parseBody(request); + context.query = bodyParseResult.query; + context.body = bodyParseResult.body; + context.files = bodyParseResult.files; + + return context; + } +} \ No newline at end of file diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart new file mode 100644 index 00000000..7268a423 --- /dev/null +++ b/lib/src/http/response_context.dart @@ -0,0 +1,129 @@ +part of angel_framework.http; + +/// A function that asynchronously generates a view from the given path and data. +typedef Future ViewGenerator(String path, {Map data}); + +/// A convenience wrapper around an outgoing HTTP request. +class ResponseContext { + /// The [Angel] instance that is sending a response. + Angel app; + + God god = new God(); + + /// Can we still write to this response? + bool isOpen = true; + + /// A set of UTF-8 encoded bytes that will be written to the response. + List> responseData = []; + + /// Sets the status code to be sent with this response. + status(int code) { + underlyingResponse.statusCode = code; + } + + /// The underlying [HttpResponse] under this instance. + HttpResponse underlyingResponse; + + ResponseContext(this.underlyingResponse); + + /// Any and all cookies to be sent to the user. + List get cookies => underlyingResponse.cookies; + + /// Set this to true if you will manually close the response. + bool willCloseItself = false; + + /// Sends a download as a response. + download(File file, {String filename}) { + header("Content-Disposition", + '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()); + } + + /// Prevents more data from being written to the response. + 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); + } + + /// Serializes JSON to the response. + json(value) { + write(god.serialize(value)); + header(HttpHeaders.CONTENT_TYPE, ContentType.JSON.toString()); + end(); + } + + /// Returns a JSONP response. + jsonp(value, {String callbackName: "callback"}) { + write("$callbackName(${god.serialize(value)})"); + header(HttpHeaders.CONTENT_TYPE, "application/javascript"); + end(); + } + + /// Renders a view to the response stream, and closes the response. + Future render(String view, {Map data}) async { + /// TODO: app.viewGenerator + var generator = app.viewGenerator(view, data: data); + write(await generator); + header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); + end(); + } + + /// Redirects to user to the given URL. + redirect(String url, {int code: 301}) { + header(HttpHeaders.LOCATION, url); + status(code); + write(''' + + + + Redirecting... + + + +

Currently redirecting you...

+
+ Click if you are not automatically redirected... + + + + '''); + end(); + } + + /// Streams a file to this response as chunked data. + /// + /// Useful for video sites. + 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 from + (HttpResponse response, Angel 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 5f91102c..eb0cec32 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -1,10 +1,59 @@ part of angel_framework.http; +typedef RouteAssigner(Pattern path, handler, {List middleware}); + /// A routable server that can handle dynamic requests. class Routable { /// Additional filters to be run on designated requests. - Map middleware = {}; + Map middleware = {}; /// Dynamic request paths that this server will respond to. - Map routes = {}; + List routes = []; + + /// A set of [Service] objects that have been mapped into routes. + Map services = {}; + + _makeRouteAssigner(String method) { + return (Pattern path, Object handler, {List middleware}) { + var route = new Route(method, path, (middleware ?? []) + ..add(handler)); + routes.add(route); + }; + } + + /// Assigns a middleware to a name for convenience. + registerMiddleware(String name, Middleware middleware) { + this.middleware[name] = middleware; + } + + /// Retrieves the service assigned to the given path. + Service service(Pattern path) => services[path]; + + /// Incorporates another routable's routes into this one's. + use(Pattern path, Routable routable) { + middleware.addAll(routable.middleware); + for (Route route in routable.routes) { + Route provisional = new Route('', path); + 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); + } + + if (routable is Service) { + services[path.toString()] = routable; + } + } + + RouteAssigner get, post, patch, delete; + + Routable() { + this.get = _makeRouteAssigner('GET'); + this.post = _makeRouteAssigner('POST'); + this.patch = _makeRouteAssigner('PATCH'); + this.delete = _makeRouteAssigner('DELETE'); + } + } \ No newline at end of file diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 669013c0..0385b091 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -1,14 +1,56 @@ part of angel_framework.http; class Route { - Pattern matcher; + RegExp matcher; String method; + List handlers = []; + String path; Route(String method, Pattern path, [List handlers]) { this.method = method; - if (path is RegExp) this.matcher = path; - else this.matcher = new RegExp('^' + - path.toString().replaceAll(new RegExp('\/'), r'\/').replaceAll( - new RegExp(':[a-zA-Z_]+'), '([^\/]+)') + r'$'); + 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(':[a-zA-Z_]+'), '([^\/]+)') + .replaceAll(new RegExp('\\*'), '.*') + + r'$'); + this.path = path; + } + + if (handlers != null) { + this.handlers.addAll(handlers); + } + } + + 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 c98ca182..c1cf874d 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -1,39 +1,136 @@ part of angel_framework.http; -/// A function that binds +/// A function that binds an [Angel] server to an Internet address and port. typedef Future ServerGenerator(InternetAddress address, int port); +/// A function that configures an [Angel] server in some way. +typedef AngelConfigurer(Angel app); + /// A powerful real-time/REST/MVC server class. class Angel extends Routable { - ServerGenerator _serverGenerator; + ServerGenerator _serverGenerator = (address, port) async => await HttpServer + .bind(address, port); + var viewGenerator = (String view, {Map data}) => {}; - _startServer(InternetAddress address, int port) async { + HttpServer httpServer; + God god = new God(); + + /// A set of custom properties that can be assigned to the server. + /// + /// Useful for configuration and extension. + Map properties = {}; + + startServer(InternetAddress address, int port) async { var server = await _serverGenerator( address ?? InternetAddress.LOOPBACK_IP_V4, port); + this.httpServer = server; var router = new Router(server); - this.routes.forEach((Route route, value) { + this.routes.forEach((Route route) { router.serve(route.matcher, method: route.method).listen(( - HttpRequest request) { + HttpRequest request) async { + RequestContext req = await RequestContext.from( + request, route.parseParameters(request.uri.toString()), this, + route); + ResponseContext res = await ResponseContext.from( + request.response, this); + bool canContinue = true; + for (var handler in route.handlers) { + if (canContinue) { + canContinue = await new Future.sync(() async { + return _applyHandler(handler, req, res); + }).catchError((e) { + stderr.write(e.error); + canContinue = false; + return false; + }); + } + } + + if (!res.willCloseItself) { + res.responseData.forEach((blob) => request.response.add(blob)); + await request.response.close(); + } }); }); + + return server; + } + + Future _applyHandler(handler, RequestContext req, + ResponseContext res) async { + if (handler is Middleware) { + return await handler(req, res); + } + + else if (handler is RequestHandler) { + await handler(req, res); + return res.isOpen; + } + + else if (handler is RawRequestHandler) { + var result = await handler(req.underlyingRequest); + return result is bool && result == true; + } + + else if (handler is Function || handler is Future) { + var result = await handler(); + return result is bool && result == true; + } + + else if (middleware.containsKey(handler)) { + return await _applyHandler(middleware[handler], req, res); + } + + else { + res.willCloseItself = true; + res.underlyingResponse.write(god.serialize(handler)); + await res.underlyingResponse.close(); + return false; + } + } + + /// Applies an [AngelConfigurer] to this instance. + void configure(AngelConfigurer configurer) { + configurer(this); } /// Starts the server. void listen({InternetAddress address, int port: 3000}) { runZoned(() async { - await _startServer(address, port); + await startServer(address, port); }, onError: onError); } /// Handles a server error. - void onError(e, [StackTrace stackTrace]) { + var onError = (e, [StackTrace stackTrace]) { + stderr.write(e.toString()); + if (stackTrace != null) + stderr.write(stackTrace.toString()); + }; - } - - Angel() {} + Angel() : super() {} /// Creates an HTTPS server. - Angel.secure() {} + Angel.secure() : super() {} + + 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 diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart new file mode 100644 index 00000000..0b598b50 --- /dev/null +++ b/lib/src/http/service.dart @@ -0,0 +1,54 @@ +part of angel_framework.http; + +/// A data store exposed to the Internet. +class Service extends Routable { + + /// Retrieves all resources. + Future index([Map params]) { + throw new MethodNotAllowedError('find'); + } + + /// Retrieves the desired resource. + Future read(id, [Map params]) { + throw new MethodNotAllowedError('get'); + } + + /// Creates a resource. + Future create(Map data, [Map params]) { + throw new MethodNotAllowedError('create'); + } + + /// Modifies a resource. + Future update(id, Map data, [Map params]) { + throw new MethodNotAllowedError('update'); + } + + /// Removes the given resource. + Future remove(id, [Map params]) { + throw new MethodNotAllowedError('remove'); + } + + Service() : super() { + get('/', (req, res) async => res.json(await this.index(req.query))); + get('/:id', (req, res) async => + res.json(await this.read(req.params['id'], req.query))); + post('/', (req, res) async => res.json(await this.create(req.body))g); + post('/:id', (req, res) async => + res.json(await this.update(req.params['id'], req.body))); + delete('/:id', (req, res) async => + res.json(await this.remove(req.params['id'], req.body))); + } +} + +/// Thrown when an unimplemented method is called. +class MethodNotAllowedError extends Error { + /// The action that threw the error. + /// + /// Ex. 'get', 'remove' + String action; + + /// A description of this error. + String get error => 'This service does not support the "$action" action.'; + + MethodNotAllowedError(String this.action); +} \ No newline at end of file diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart new file mode 100644 index 00000000..c367340c --- /dev/null +++ b/lib/src/http/services/memory.dart @@ -0,0 +1,31 @@ +part of angel_framework.http; + +/// An in-memory [Service]. +class MemoryService extends Service { + God god = new God(); + Map items = {}; + + Future index([Map params]) async => items.values.toList(); + + Future read(id, [Map params]) async => items[int.parse(id)]; + + Future create(Map data, [Map params]) async { + data['id'] = items.length; + items[items.length] = god.deserializeFromMap(data, T); + return items[items.length - 1]; + } + + Future update(id, Map data, [Map params]) async { + data['id'] = int.parse(id); + items[int.parse(id)] = god.deserializeFromMap(data, T); + return data; + } + + Future remove(id, [Map params]) async { + var item = items[int.parse(id)]; + items.remove(int.parse(id)); + return item; + } + + MemoryService() : super(); +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 86b44c74..ae48993a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,5 +3,10 @@ version: 0.0.1-dev description: Core libraries for the Angel framework. author: Tobe O dependencies: + body_parser: ^1.0.0-dev json_god: any - route: any \ No newline at end of file + mime: ^0.9.3 + route: any +dev_dependencies: + http: any + test: any \ No newline at end of file diff --git a/test/routing.dart b/test/routing.dart new file mode 100644 index 00000000..62eb003b --- /dev/null +++ b/test/routing.dart @@ -0,0 +1,72 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart'; +import 'package:test/test.dart'; + +main() { + group('routing', () { + Angel angel; + Angel nested; + Angel todos; + String url; + http.Client client; + God god; + + setUp(() async { + god = new God(); + angel = new Angel(); + nested = new Angel(); + todos = new Angel(); + + todos.get('/action/:action', (req, res) => res.json(req.params)); + + nested.post('/ted/:route', (req, res) => res.json(req.params)); + + angel.get('/hello', 'world'); + angel.get('/name/:first/last/:last', (req, res) => res.json(req.params)); + angel.use('/nes', nested); + angel.use('/todos/:id', todos); + + client = new http.Client(); + await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + }); + + tearDown(() async { + await angel.httpServer.close(force: true); + angel = null; + nested = null; + todos = null; + client.close(); + client = null; + url = null; + god = null; + }); + + test('Can match basic url', () async { + var response = await client.get("$url/hello"); + expect(response.body, equals('"world"')); + }); + + test('Can match url with multiple parameters', () async { + var response = await client.get('$url/name/HELLO/last/WORLD'); + var json = god.deserialize(response.body); + expect(json['first'], equals('HELLO')); + expect(json['last'], equals('WORLD')); + }); + + test('Can nest another Angel instance', () async { + var response = await client.post('$url/nes/ted/foo'); + var json = god.deserialize(response.body); + expect(json['route'], equals('foo')); + }); + + 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); + expect(json['id'], equals(1337)); + expect(json['action'], equals('test')); + }); + }); +} \ No newline at end of file diff --git a/test/services.dart b/test/services.dart new file mode 100644 index 00000000..11157293 --- /dev/null +++ b/test/services.dart @@ -0,0 +1,54 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart'; +import 'package:test/test.dart'; + +class Todo { + int id; + String text; +} + +main() { + group('Utilities', () { + Map headers = { + 'Content-Type': 'application/json' + }; + Angel angel; + String url; + http.Client client; + God god; + + setUp(() async { + angel = new Angel(); + client = new http.Client(); + god = new God(); + angel.use('/todos', new MemoryService()); + await angel.startServer(null, 0); + url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + }); + + tearDown(() async { + angel = null; + url = null; + client.close(); + client = null; + god = null; + }); + + group('memory', () { + test('can index an empty service', () async { + var response = await client.get("$url/todos/"); + expect(response.body, equals('[]')); + }); + + test('can create data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + var response = await client.post( + "$url/todos/", headers: headers, body: postData); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('Hello, world!')); + }); + }); + }); +} \ No newline at end of file diff --git a/test/util.dart b/test/util.dart new file mode 100644 index 00000000..6e641c0a --- /dev/null +++ b/test/util.dart @@ -0,0 +1,32 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:test/test.dart'; + +class Foo { + String name; + + Foo(String this.name); +} + +main() { + group('Utilities', () { + Angel angel; + + setUp(() { + angel = new Angel(); + }); + + tearDown(() { + angel = null; + }); + + test('can use app.properties like members', () { + angel.properties['hello'] = 'world'; + 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')); + }); + }); +} \ No newline at end of file From d51ab2e2a1e49a6bea87e44ade2e10331592b44e Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 21 Apr 2016 16:37:02 -0400 Subject: [PATCH 003/414] It's getting there. Slowly, but surely. --- LICENSE | 674 +++++++++++++++++++++++++++++ lib/src/http/extensible.dart | 26 ++ lib/src/http/http.dart | 1 + lib/src/http/request_context.dart | 2 +- lib/src/http/response_context.dart | 2 +- lib/src/http/routable.dart | 5 +- lib/src/http/route.dart | 1 + lib/src/http/server.dart | 48 +- lib/src/http/service.dart | 2 +- pubspec.yaml | 15 +- test/routing.dart | 23 +- test/services.dart | 2 +- 12 files changed, 758 insertions(+), 43 deletions(-) create mode 100644 LICENSE create mode 100644 lib/src/http/extensible.dart diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..5717dd98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + +{one line to give the program's name and a brief idea of what it does.} +Copyright (C) {year} {name of author} + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + +{project} Copyright (C) {year} {fullname} +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/lib/src/http/extensible.dart b/lib/src/http/extensible.dart new file mode 100644 index 00000000..ec485e3a --- /dev/null +++ b/lib/src/http/extensible.dart @@ -0,0 +1,26 @@ +part of angel_framework.http; + +/// Basically an Expando whoops +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 diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index dd0d4a70..e42cace5 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -10,6 +10,7 @@ import 'package:json_god/json_god.dart'; import 'package:mime/mime.dart'; import 'package:route/server.dart'; +part 'extensible.dart'; part 'request_context.dart'; part 'response_context.dart'; part 'route.dart'; diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index 35bb7e23..1fd8980b 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -10,7 +10,7 @@ typedef Future RequestHandler(RequestContext req, ResponseContext res); typedef Future RawRequestHandler(HttpRequest request); /// A convenience wrapper around an incoming HTTP request. -class RequestContext { +class RequestContext extends Extensible { /// The [Angel] instance that is responding to this request. Angel app; diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 7268a423..ea4a688b 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -4,7 +4,7 @@ part of angel_framework.http; typedef Future ViewGenerator(String path, {Map data}); /// A convenience wrapper around an outgoing HTTP request. -class ResponseContext { +class ResponseContext extends Extensible { /// The [Angel] instance that is sending a response. Angel app; diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index eb0cec32..4c3abe67 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -1,9 +1,9 @@ part of angel_framework.http; -typedef RouteAssigner(Pattern path, handler, {List middleware}); +typedef Route RouteAssigner(Pattern path, handler, {List middleware}); /// A routable server that can handle dynamic requests. -class Routable { +class Routable extends Extensible { /// Additional filters to be run on designated requests. Map middleware = {}; @@ -18,6 +18,7 @@ class Routable { var route = new Route(method, path, (middleware ?? []) ..add(handler)); routes.add(route); + return route; }; } diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 0385b091..baf03314 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -5,6 +5,7 @@ class Route { String method; List handlers = []; String path; + String name; Route(String method, Pattern path, [List handlers]) { this.method = method; diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index c1cf874d..9f64ef26 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -15,11 +15,6 @@ class Angel extends Routable { HttpServer httpServer; God god = new God(); - /// A set of custom properties that can be assigned to the server. - /// - /// Useful for configuration and extension. - Map properties = {}; - startServer(InternetAddress address, int port) async { var server = await _serverGenerator( address ?? InternetAddress.LOOPBACK_IP_V4, port); @@ -61,22 +56,38 @@ class Angel extends Routable { Future _applyHandler(handler, RequestContext req, ResponseContext res) async { if (handler is Middleware) { - return await handler(req, res); + var result = await handler(req, res); + if (result is bool) + return result == true; + else if (result != null) { + res.json(result); + return false; + } else return true; } - else if (handler is RequestHandler) { + if (handler is RequestHandler) { await handler(req, res); return res.isOpen; } else if (handler is RawRequestHandler) { var result = await handler(req.underlyingRequest); - return result is bool && result == true; + if (result is bool) + return result == true; + else if (result != null) { + res.json(result); + return false; + } else return true; } else if (handler is Function || handler is Future) { var result = await handler(); - return result is bool && result == true; + if (result is bool) + return result == true; + else if (result != null) { + res.json(result); + return false; + } else return true; } else if (middleware.containsKey(handler)) { @@ -114,23 +125,4 @@ class Angel extends Routable { /// Creates an HTTPS server. Angel.secure() : super() {} - - 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 diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 0b598b50..a33bdafa 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -32,7 +32,7 @@ class Service extends Routable { get('/', (req, res) async => res.json(await this.index(req.query))); get('/:id', (req, res) async => res.json(await this.read(req.params['id'], req.query))); - post('/', (req, res) async => res.json(await this.create(req.body))g); + post('/', (req, res) async => res.json(await this.create(req.body))); post('/:id', (req, res) async => res.json(await this.update(req.params['id'], req.body))); delete('/:id', (req, res) async => diff --git a/pubspec.yaml b/pubspec.yaml index ae48993a..0087e98e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,13 @@ name: angel_framework -version: 0.0.1-dev +version: 0.0.0-dev description: Core libraries for the Angel framework. author: Tobe O +homepage: https://github.com/angel-dart/angel_framework dependencies: - body_parser: ^1.0.0-dev - json_god: any - mime: ^0.9.3 - route: any + body_parser: ">=1.0.0-dev <2.0.0" + json_god: ">=1.0.0 <2.0.0" + mime: ">=0.9.3 <0.10.0" + route: ">= 0.4.6 <0.5.0" dev_dependencies: - http: any - test: any \ No newline at end of file + http: ^0.11.3 + test: ^0.12.13 \ No newline at end of file diff --git a/test/routing.dart b/test/routing.dart index 62eb003b..7ce95cb0 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -19,12 +19,18 @@ main() { nested = new Angel(); todos = new Angel(); + angel.registerMiddleware('interceptor', (req, res) async { + res.write('Middleware'); + return false; + }); + todos.get('/action/:action', (req, res) => res.json(req.params)); - nested.post('/ted/:route', (req, res) => res.json(req.params)); - + angel.get('/intercepted', 'This should not be shown', + middleware: ['interceptor']); angel.get('/hello', 'world'); angel.get('/name/:first/last/:last', (req, res) => res.json(req.params)); + angel.post('/lambda', (req, res) => req.body); angel.use('/nes', nested); angel.use('/todos/:id', todos); @@ -68,5 +74,18 @@ main() { expect(json['id'], equals(1337)); expect(json['action'], equals('test')); }); + + test('Can add and use named middleware', () async { + var response = await client.get('$url/intercepted'); + expect(response.body, equals('Middleware')); + }); + + test('Can serialize function result as JSON', () async { + Map headers = {'Content-Type': 'application/json'}; + String postData = god.serialize({'it': 'works'}); + var response = await client.post( + "$url/lambda", headers: headers, body: postData); + expect(god.deserialize(response.body)['it'], equals('works')); + }); }); } \ No newline at end of file diff --git a/test/services.dart b/test/services.dart index 11157293..c7d5417c 100644 --- a/test/services.dart +++ b/test/services.dart @@ -9,7 +9,7 @@ class Todo { } main() { - group('Utilities', () { + group('Services', () { Map headers = { 'Content-Type': 'application/json' }; From 04f1a2558bd99f9c72f130690b48e334141a44b5 Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 21:27:28 -0400 Subject: [PATCH 004/414] View generator --- lib/src/http/response_context.dart | 6 ++---- lib/src/http/server.dart | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index ea4a688b..ef081105 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -18,7 +18,7 @@ class ResponseContext extends Extensible { /// Sets the status code to be sent with this response. status(int code) { - underlyingResponse.statusCode = code; + underlyingResponse.statusCode = code; } /// The underlying [HttpResponse] under this instance. @@ -66,9 +66,7 @@ class ResponseContext extends Extensible { /// Renders a view to the response stream, and closes the response. Future render(String view, {Map data}) async { - /// TODO: app.viewGenerator - var generator = app.viewGenerator(view, data: data); - write(await generator); + write(await app.viewGenerator(view, data: data)); header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); end(); } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 9f64ef26..adf11048 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -10,7 +10,7 @@ typedef AngelConfigurer(Angel app); class Angel extends Routable { ServerGenerator _serverGenerator = (address, port) async => await HttpServer .bind(address, port); - var viewGenerator = (String view, {Map data}) => {}; + var viewGenerator = (String view, {Map data}) => "No view engine has been configured yet."; HttpServer httpServer; God god = new God(); diff --git a/pubspec.yaml b/pubspec.yaml index 0087e98e..2feca39e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,5 +9,5 @@ dependencies: mime: ">=0.9.3 <0.10.0" route: ">= 0.4.6 <0.5.0" dev_dependencies: - http: ^0.11.3 - test: ^0.12.13 \ No newline at end of file + http: ">= 0.11.3 < 0.12.0" + test: ">= 0.12.13 < 0.13.0" \ No newline at end of file From d4d5678f646d01f1eb5a1c5cd42476fc65ed5036 Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 21:27:54 -0400 Subject: [PATCH 005/414] dev.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2feca39e..e47bbb6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev +version: 0.0.0-dev.1 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From f993155c04ddaa8259feed8b8cfea075ef54245b Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 21:42:39 -0400 Subject: [PATCH 006/414] dev.2 --- lib/src/http/server.dart | 32 +++++++++++++++++++++++++------- pubspec.yaml | 2 +- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index adf11048..0662297d 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -10,7 +10,8 @@ typedef AngelConfigurer(Angel app); class Angel extends Routable { ServerGenerator _serverGenerator = (address, port) async => await HttpServer .bind(address, port); - var viewGenerator = (String view, {Map data}) => "No view engine has been configured yet."; + var viewGenerator = (String view, + {Map data}) => "No view engine has been configured yet."; HttpServer httpServer; God god = new God(); @@ -43,13 +44,20 @@ class Angel extends Routable { } } - if (!res.willCloseItself) { - res.responseData.forEach((blob) => request.response.add(blob)); - await request.response.close(); - } + _finalizeResponse(request, res); }); }); + router.defaultStream.listen((HttpRequest request) async { + RequestContext req = await RequestContext.from( + request, {}, this, + null); + ResponseContext res = await ResponseContext.from( + request.response, this); + on404(req, res); + _finalizeResponse(request, res); + }); + return server; } @@ -102,6 +110,13 @@ class Angel extends Routable { } } + _finalizeResponse(HttpRequest request, ResponseContext res) async { + if (!res.willCloseItself) { + res.responseData.forEach((blob) => request.response.add(blob)); + await request.response.close(); + } + } + /// Applies an [AngelConfigurer] to this instance. void configure(AngelConfigurer configurer) { configurer(this); @@ -114,12 +129,15 @@ class Angel extends Routable { }, onError: onError); } + /// Responds to a 404. + RequestHandler on404 = (req, res) => res.write("404 Not Found"); + /// Handles a server error. - var onError = (e, [StackTrace stackTrace]) { + onError(e, [StackTrace stackTrace]) { stderr.write(e.toString()); if (stackTrace != null) stderr.write(stackTrace.toString()); - }; + } Angel() : super() {} diff --git a/pubspec.yaml b/pubspec.yaml index e47bbb6d..a1385bbb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.1 +version: 0.0.0-dev.2 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 608a8f3641076d8d5a3b5af5d2a45a0b75fe6553 Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 21:44:59 -0400 Subject: [PATCH 007/414] dev.3 --- lib/src/http/server.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 0662297d..b160f713 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -54,7 +54,7 @@ class Angel extends Routable { null); ResponseContext res = await ResponseContext.from( request.response, this); - on404(req, res); + on404(req, res..status(404)); _finalizeResponse(request, res); }); diff --git a/pubspec.yaml b/pubspec.yaml index a1385bbb..28fb342c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.2 +version: 0.0.0-dev.3 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From c2de78db0c0f17a72467d023588a2690cacb7dbf Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 22:40:37 -0400 Subject: [PATCH 008/414] dev.5? --- lib/src/http/response_context.dart | 4 ++-- lib/src/http/server.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index ef081105..5e992f36 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -65,8 +65,8 @@ class ResponseContext extends Extensible { } /// Renders a view to the response stream, and closes the response. - Future render(String view, {Map data}) async { - write(await app.viewGenerator(view, data: data)); + Future render(String view, [Map data]) async { + write(await app.viewGenerator(view, data)); header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); end(); } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index b160f713..ef22018d 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -11,7 +11,7 @@ class Angel extends Routable { ServerGenerator _serverGenerator = (address, port) async => await HttpServer .bind(address, port); var viewGenerator = (String view, - {Map data}) => "No view engine has been configured yet."; + [Map data]) => "No view engine has been configured yet."; HttpServer httpServer; God god = new God(); @@ -54,7 +54,7 @@ class Angel extends Routable { null); ResponseContext res = await ResponseContext.from( request.response, this); - on404(req, res..status(404)); + on404(req, res); _finalizeResponse(request, res); }); diff --git a/pubspec.yaml b/pubspec.yaml index 28fb342c..847c8ac0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.3 +version: 0.0.0-dev.5 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 93a29c43cfcd5670740362faa97dc3240ff15c0e Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 28 Apr 2016 20:01:58 -0400 Subject: [PATCH 009/414] Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented --- README.md | 2 + lib/src/http/errors.dart | 96 +++++++++++++++ lib/src/http/http.dart | 4 +- lib/src/http/routable.dart | 52 +++++--- lib/src/http/server.dart | 193 ++++++++++++++++++++---------- lib/src/http/service.dart | 50 ++++---- lib/src/http/services/memory.dart | 66 ++++++++-- pubspec.yaml | 4 +- test/routing.dart | 8 +- test/services.dart | 68 ++++++++++- 10 files changed, 425 insertions(+), 118 deletions(-) create mode 100644 README.md create mode 100644 lib/src/http/errors.dart diff --git a/README.md b/README.md new file mode 100644 index 00000000..4583a5d4 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# angel_framework +Documentation in the works. \ No newline at end of file diff --git a/lib/src/http/errors.dart b/lib/src/http/errors.dart new file mode 100644 index 00000000..512d1daf --- /dev/null +++ b/lib/src/http/errors.dart @@ -0,0 +1,96 @@ +part of angel_framework.http; + +class _AngelHttpExceptionBase implements Exception { + int statusCode; + String message; + List errors; + + _AngelHttpExceptionBase.base() {} + + _AngelHttpExceptionBase(this.statusCode, this.message, + {List this.errors: const []}); + + @override + String toString() { + return "$statusCode: $message"; + } + + Map toMap() { + return { + 'isError': true, + 'statusCode': statusCode, + 'message': message, + 'errors': errors + }; + } +} + +/// Basically the same as +/// [feathers-errors](https://github.com/feathersjs/feathers-errors). +class AngelHttpException extends _AngelHttpExceptionBase { + /// Throws a 500 Internal Server Error. + /// Set includeRealException to true to print include the actual exception along with + /// this error. Useful flag for development vs. production. + AngelHttpException(Exception exception, + {bool includeRealException: false, StackTrace stackTrace}) :super.base() { + statusCode = 500; + message = "500 Internal Server Error"; + if (includeRealException) { + errors.add(exception.toString()); + if (stackTrace != null) { + errors.add(stackTrace.toString()); + } + } + } + + /// Throws a 400 Bad Request error, including an optional arrray of (validation?) + /// errors you specify. + AngelHttpException.BadRequest( + {String message: '400 Bad Request', List errors: const[]}) + : super(400, message, errors: errors); + + /// Throws a 401 Not Authenticated error. + AngelHttpException.NotAuthenticated({String message: '401 Not Authenticated'}) + : super(401, message); + + /// Throws a 402 Payment Required error. + AngelHttpException.PaymentRequired({String message: '402 Payment Required'}) + : super(402, message); + + /// Throws a 403 Forbidden error. + AngelHttpException.Forbidden({String message: '403 Forbidden'}) + : super(403, message); + + /// Throws a 404 Not Found error. + AngelHttpException.NotFound({String message: '404 Not Found'}) + : super(404, message); + + /// Throws a 405 Method Not Allowed error. + AngelHttpException.MethodNotAllowed( + {String message: '405 Method Not Allowed'}) + : super(405, message); + + /// Throws a 406 Not Acceptable error. + AngelHttpException.NotAcceptable({String message: '406 Not Acceptable'}) + : super(406, message); + + /// Throws a 408 Timeout error. + AngelHttpException.MethodTimeout({String message: '408 Timeout'}) + : super(408, message); + + /// Throws a 409 Conflict error. + AngelHttpException.Conflict({String message: '409 Conflict'}) + : super(409, message); + + /// Throws a 422 Not Processable error. + AngelHttpException.NotProcessable({String message: '422 Not Processable'}) + : super(422, message); + + /// Throws a 501 Not Implemented error. + AngelHttpException.NotImplemented({String message: '501 Not Implemented'}) + : super(501, message); + + /// Throws a 503 Unavailable error. + AngelHttpException.Unavailable({String message: '503 Unavailable'}) + : super(503, message); +} \ No newline at end of file diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index e42cace5..6b94ce67 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -4,13 +4,15 @@ library angel_framework.http; import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'dart:mirrors'; import 'package:body_parser/body_parser.dart'; import 'package:json_god/json_god.dart'; +import 'package:merge_map/merge_map.dart'; import 'package:mime/mime.dart'; -import 'package:route/server.dart'; part 'extensible.dart'; +part 'errors.dart'; part 'request_context.dart'; part 'response_context.dart'; part 'route.dart'; diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 4c3abe67..23f3fce5 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -13,15 +13,6 @@ class Routable extends Extensible { /// A set of [Service] objects that have been mapped into routes. Map services = {}; - _makeRouteAssigner(String method) { - return (Pattern path, Object handler, {List middleware}) { - var route = new Route(method, path, (middleware ?? []) - ..add(handler)); - routes.add(route); - return route; - }; - } - /// Assigns a middleware to a name for convenience. registerMiddleware(String name, Middleware middleware) { this.middleware[name] = middleware; @@ -35,6 +26,10 @@ class Routable extends Extensible { middleware.addAll(routable.middleware); 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'\$$'), ''))); @@ -48,13 +43,42 @@ class Routable extends Extensible { } } - RouteAssigner get, post, patch, delete; + /// Adds a route that responds to the given path + /// for requests with the given method (case-insensitive). + /// Provide '*' as the method to respond to all methods. + addRoute(String method, Pattern path, Object handler, {List middleware}) { + var route = new Route(method.toUpperCase().trim(), path, (middleware ?? []) + ..add(handler)); + routes.add(route); + return route; + } + + /// Adds a route that responds to any request matching the given path. + all(Pattern path, Object handler, {List middleware}) { + return addRoute('*', path, handler, middleware: middleware); + } + + /// Adds a route that responds to a GET request. + get(Pattern path, Object handler, {List middleware}) { + return addRoute('GET', path, handler, middleware: middleware); + } + + /// Adds a route that responds to a POST request. + post(Pattern path, Object handler, {List middleware}) { + return addRoute('POST', path, handler, middleware: middleware); + } + + /// Adds a route that responds to a PATCH request. + patch(Pattern path, Object handler, {List middleware}) { + return addRoute('PATCH', path, handler, middleware: middleware); + } + + /// Adds a route that responds to a DELETE request. + delete(Pattern path, Object handler, {List middleware}) { + return addRoute('DELETE', path, handler, middleware: middleware); + } Routable() { - this.get = _makeRouteAssigner('GET'); - this.post = _makeRouteAssigner('POST'); - this.patch = _makeRouteAssigner('PATCH'); - this.delete = _makeRouteAssigner('DELETE'); } } \ No newline at end of file diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index ef22018d..a8cc858d 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -4,57 +4,97 @@ part of angel_framework.http; typedef Future ServerGenerator(InternetAddress address, int port); /// A function that configures an [Angel] server in some way. -typedef AngelConfigurer(Angel app); +typedef Future AngelConfigurer(Angel app); /// A powerful real-time/REST/MVC server class. class Angel extends Routable { - ServerGenerator _serverGenerator = (address, port) async => await HttpServer - .bind(address, port); - var viewGenerator = (String view, - [Map data]) => "No view engine has been configured yet."; + ServerGenerator _serverGenerator = + (address, port) async => await HttpServer.bind(address, port); + + /// Default error handler, show HTML error page + var _errorHandler = (AngelHttpException e, req, ResponseContext res) { + res.status(e.statusCode); + res.write("${e.message}"); + res.write("

${e.message}

    "); + for (String error in e.errors) { + res.write("
  • $error
  • "); + } + res.write("
"); + res.end(); + }; + + var viewGenerator = + (String view, [Map data]) => "No view engine has been configured yet."; + + /// [Middleware] to be run before all requests. + List before = []; + + /// [Middleware] to be run after all requests. + List after = []; HttpServer httpServer; God god = new God(); startServer(InternetAddress address, int port) async { - var server = await _serverGenerator( - address ?? InternetAddress.LOOPBACK_IP_V4, port); + var server = + await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); this.httpServer = server; - var router = new Router(server); - this.routes.forEach((Route route) { - router.serve(route.matcher, method: route.method).listen(( - HttpRequest request) async { - RequestContext req = await RequestContext.from( - request, route.parseParameters(request.uri.toString()), this, - route); - ResponseContext res = await ResponseContext.from( - request.response, this); - bool canContinue = true; + server.listen((HttpRequest request) async { + String req_url = + request.uri.toString().replaceAll(new RegExp(r'\/+$'), ''); + RequestContext req = await RequestContext.from(request, {}, this, null); + ResponseContext res = await ResponseContext.from(request.response, this); - for (var handler in route.handlers) { - if (canContinue) { - canContinue = await new Future.sync(() async { - return _applyHandler(handler, req, res); - }).catchError((e) { - stderr.write(e.error); - canContinue = false; - return false; - }); + bool canContinue = true; + + var execHandler = (handler, req) async { + if (canContinue) { + canContinue = await new Future.sync(() async { + return _applyHandler(handler, req, res); + }).catchError((e, [StackTrace stackTrace]) async { + if (e is AngelHttpException) { + // Special handling for AngelHttpExceptions :) + try { + String accept = request.headers.value(HttpHeaders.ACCEPT) ?? "*/*"; + if (accept == "*/*" || + accept.contains("application/json") || + accept.contains("application/javascript")) { + res.json(e.toMap()); + } else { + await _applyHandler(_errorHandler, req, res); + } + } catch (_) { + } + } + _onError(e, stackTrace); + canContinue = false; + return false; + }); + } else + return false; + }; + + for (var handler in before) { + await execHandler(handler, req); + } + + for (Route route in routes) { + if (!canContinue) break; + if (route.matcher.hasMatch(req_url) && + (request.method == route.method || route.method == '*')) { + req.params = route.parseParameters(request.uri.toString()); + req.route = route; + + for (var handler in route.handlers) { + await execHandler(handler, req); } } + } - _finalizeResponse(request, res); - }); - }); - - router.defaultStream.listen((HttpRequest request) async { - RequestContext req = await RequestContext.from( - request, {}, this, - null); - ResponseContext res = await ResponseContext.from( - request.response, this); - on404(req, res); + for (var handler in after) { + await execHandler(handler, req); + } _finalizeResponse(request, res); }); @@ -70,39 +110,34 @@ class Angel extends Routable { else if (result != null) { res.json(result); return false; - } else return true; + } else + return res.isOpen; } if (handler is RequestHandler) { await handler(req, res); return res.isOpen; - } - - else if (handler is RawRequestHandler) { + } else if (handler is RawRequestHandler) { var result = await handler(req.underlyingRequest); if (result is bool) return result == true; else if (result != null) { res.json(result); return false; - } else return true; - } - - else if (handler is Function || handler is Future) { + } else + return true; + } else if (handler is Function || handler is Future) { var result = await handler(); if (result is bool) return result == true; else if (result != null) { res.json(result); return false; - } else return true; - } - - else if (middleware.containsKey(handler)) { + } else + return true; + } else if (middleware.containsKey(handler)) { return await _applyHandler(middleware[handler], req, res); - } - - else { + } else { res.willCloseItself = true; res.underlyingResponse.write(god.serialize(handler)); await res.underlyingResponse.close(); @@ -117,30 +152,64 @@ class Angel extends Routable { } } + String _randomString(int length) { + var rand = new Random(); + var codeUnits = new List.generate(length, (index) { + return rand.nextInt(33) + 89; + }); + + return new String.fromCharCodes(codeUnits); + } + /// Applies an [AngelConfigurer] to this instance. - void configure(AngelConfigurer configurer) { - configurer(this); + Future configure(AngelConfigurer configurer) async { + await configurer(this); } /// Starts the server. void listen({InternetAddress address, int port: 3000}) { runZoned(() async { await startServer(address, port); - }, onError: onError); + }, onError: _onError); } - /// Responds to a 404. - RequestHandler on404 = (req, res) => res.write("404 Not Found"); + @override + use(Pattern path, Routable routable) { + if (routable is Service) { + routable.app = this; + } + super.use(path, routable); + } + + onError(handler) { + _errorHandler = handler; + } /// Handles a server error. - onError(e, [StackTrace stackTrace]) { + _onError(e, [StackTrace stackTrace]) { stderr.write(e.toString()); - if (stackTrace != null) - stderr.write(stackTrace.toString()); + if (stackTrace != null) stderr.write(stackTrace.toString()); } Angel() : super() {} /// Creates an HTTPS server. - Angel.secure() : super() {} -} \ No newline at end of file + /// Provide paths to a certificate chain and server key (both .pem). + /// If no password is provided, a random one will be generated upon running + /// the server. + Angel.secure(String certificateChainPath, String serverKeyPath, + {String password}) + : super() { + _serverGenerator = (InternetAddress address, int port) async { + var certificateChain = + Platform.script.resolve('server_chain.pem').toFilePath(); + var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); + var serverContext = new SecurityContext(); + serverContext.useCertificateChain(certificateChain); + serverContext.usePrivateKey(serverKey, + password: password ?? _randomString(8)); + + return await HttpServer.bindSecure(address, port, serverContext); + }; + } +} diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index a33bdafa..2f9ea3e3 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -2,53 +2,53 @@ part of angel_framework.http; /// A data store exposed to the Internet. class Service extends Routable { + /// The [Angel] app powering this service. + Angel app; /// Retrieves all resources. Future index([Map params]) { - throw new MethodNotAllowedError('find'); + throw new AngelHttpException.MethodNotAllowed(); } /// Retrieves the desired resource. Future read(id, [Map params]) { - throw new MethodNotAllowedError('get'); + throw new AngelHttpException.MethodNotAllowed(); } /// Creates a resource. Future create(Map data, [Map params]) { - throw new MethodNotAllowedError('create'); + throw new AngelHttpException.MethodNotAllowed(); } /// Modifies a resource. + Future modify(id, Map data, [Map params]) { + throw new AngelHttpException.MethodNotAllowed(); + } + + /// Overwrites a resource. Future update(id, Map data, [Map params]) { - throw new MethodNotAllowedError('update'); + throw new AngelHttpException.MethodNotAllowed(); } /// Removes the given resource. Future remove(id, [Map params]) { - throw new MethodNotAllowedError('remove'); + throw new AngelHttpException.MethodNotAllowed(); } Service() : super() { - get('/', (req, res) async => res.json(await this.index(req.query))); + get('/', (req, res) async => await this.index(req.query)); + + post('/', (req, res) async => await this.create(req.body)); + get('/:id', (req, res) async => - res.json(await this.read(req.params['id'], req.query))); - post('/', (req, res) async => res.json(await this.create(req.body))); - post('/:id', (req, res) async => - res.json(await this.update(req.params['id'], req.body))); - delete('/:id', (req, res) async => - res.json(await this.remove(req.params['id'], req.body))); + await this.read(req.params['id'], req.query)); + + patch('/:id', (req, res) async => await this.modify( + req.params['id'], req.body)); + + post('/:id', (req, res) async => await this.update( + req.params['id'], req.body)); + + delete('/:id', (req, res) async => await this.remove(req.params['id'], req.query)); } -} - -/// Thrown when an unimplemented method is called. -class MethodNotAllowedError extends Error { - /// The action that threw the error. - /// - /// Ex. 'get', 'remove' - String action; - - /// A description of this error. - String get error => 'This service does not support the "$action" action.'; - - MethodNotAllowedError(String this.action); } \ No newline at end of file diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index c367340c..987c5a76 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -5,26 +5,70 @@ class MemoryService extends Service { God god = new God(); Map items = {}; - Future index([Map params]) async => items.values.toList(); + Map makeJson(int index, T t) { + return mergeMap([god.serializeToMap(t), {'id': index}]); + } - Future read(id, [Map params]) async => items[int.parse(id)]; + Future index([Map params]) async { + return items.keys + .where((index) => items[index] != null) + .map((index) => makeJson(index, items[index])) + .toList(); + } + + Future read(id, [Map params]) async { + int desiredId = int.parse(id.toString()); + if (items.containsKey(desiredId)) { + T found = items[desiredId]; + if (found != null) { + return makeJson(desiredId, found); + } else throw new AngelHttpException.NotFound(); + } else throw new AngelHttpException.NotFound(); + } Future create(Map data, [Map params]) async { - data['id'] = items.length; - items[items.length] = god.deserializeFromMap(data, T); - return items[items.length - 1]; + try { + items[items.length] = god.deserializeFromMap(data, T); + T created = items[items.length - 1]; + return makeJson(items.length - 1, created); + } catch (e) { + throw new AngelHttpException.BadRequest(message: 'Invalid data.'); + } + } + + Future modify(id, Map data, [Map params]) async { + int desiredId = int.parse(id.toString()); + if (items.containsKey(desiredId)) { + try { + Map existing = god.serializeToMap(items[desiredId]); + data = mergeMap([existing, data]); + items[desiredId] = god.deserializeFromMap(data, T); + return makeJson(desiredId, items[desiredId]); + } catch (e) { + throw new AngelHttpException.BadRequest(message: 'Invalid data.'); + } + } else throw new AngelHttpException.NotFound(); } Future update(id, Map data, [Map params]) async { - data['id'] = int.parse(id); - items[int.parse(id)] = god.deserializeFromMap(data, T); - return data; + int desiredId = int.parse(id.toString()); + if (items.containsKey(desiredId)) { + try { + items[desiredId] = god.deserializeFromMap(data, T); + return makeJson(desiredId, items[desiredId]); + } catch (e) { + throw new AngelHttpException.BadRequest(message: 'Invalid data.'); + } + } else throw new AngelHttpException.NotFound(); } Future remove(id, [Map params]) async { - var item = items[int.parse(id)]; - items.remove(int.parse(id)); - return item; + int desiredId = int.parse(id.toString()); + if (items.containsKey(desiredId)) { + T item = items[desiredId]; + items[desiredId] = null; + return makeJson(desiredId, item); + } else throw new AngelHttpException.NotFound(); } MemoryService() : super(); diff --git a/pubspec.yaml b/pubspec.yaml index 847c8ac0..4cce9475 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: angel_framework -version: 0.0.0-dev.5 +version: 0.0.0-dev.6 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework dependencies: body_parser: ">=1.0.0-dev <2.0.0" json_god: ">=1.0.0 <2.0.0" + merge_map: ">=1.0.0 <2.0.0" mime: ">=0.9.3 <0.10.0" - route: ">= 0.4.6 <0.5.0" dev_dependencies: http: ">= 0.11.3 < 0.12.0" test: ">= 0.12.13 < 0.13.0" \ No newline at end of file diff --git a/test/routing.dart b/test/routing.dart index 7ce95cb0..cf6950e3 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -29,10 +29,11 @@ main() { angel.get('/intercepted', 'This should not be shown', middleware: ['interceptor']); angel.get('/hello', 'world'); - angel.get('/name/:first/last/:last', (req, res) => res.json(req.params)); + angel.get('/name/:first/last/:last', (req, res) => req.params); angel.post('/lambda', (req, res) => req.body); angel.use('/nes', nested); angel.use('/todos/:id', todos); + angel.get('*', 'MJ'); client = new http.Client(); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); @@ -87,5 +88,10 @@ main() { "$url/lambda", headers: headers, body: postData); expect(god.deserialize(response.body)['it'], equals('works')); }); + + test('Fallback routes', () async { + var response = await client.get('$url/my_favorite_artist'); + expect(response.body, equals('"MJ"')); + }); }); } \ No newline at end of file diff --git a/test/services.dart b/test/services.dart index c7d5417c..af07a06d 100644 --- a/test/services.dart +++ b/test/services.dart @@ -4,13 +4,14 @@ import 'package:json_god/json_god.dart'; import 'package:test/test.dart'; class Todo { - int id; String text; + String over; } main() { group('Services', () { Map headers = { + 'Accept': 'application/json', 'Content-Type': 'application/json' }; Angel angel; @@ -38,17 +39,80 @@ main() { group('memory', () { test('can index an empty service', () async { var response = await client.get("$url/todos/"); + print(response.body); expect(response.body, equals('[]')); + for (int i = 0; i < 3; i++) { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + } + response = await client.get("$url/todos"); + print(response.body); + expect(god + .deserialize(response.body) + .length, equals(3)); }); test('can create data', () async { String postData = god.serialize({'text': 'Hello, world!'}); var response = await client.post( - "$url/todos/", headers: headers, body: postData); + "$url/todos", headers: headers, body: postData); var json = god.deserialize(response.body); print(json); expect(json['text'], equals('Hello, world!')); }); + + test('can fetch data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + var response = await client.get( + "$url/todos/0"); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('Hello, world!')); + }); + + test('can modify data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + postData = god.serialize({'text': 'modified'}); + var response = await client.patch( + "$url/todos/0", headers: headers, body: postData); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('modified')); + }); + + test('can overwrite data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + postData = god.serialize({'over': 'write'}); + var response = await client.post( + "$url/todos/0", headers: headers, body: postData); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals(null)); + expect(json['over'], equals('write')); + }); + + test('can delete data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + var response = await client.delete( + "$url/todos/0"); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('Hello, world!')); + response = await client.get("$url/todos"); + print(response.body); + expect(god + .deserialize(response.body) + .length, equals(0)); + }); }); }); } \ No newline at end of file From 0de6c5234f6056a8617fbc89f9188d7b69877a33 Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 28 Apr 2016 20:12:57 -0400 Subject: [PATCH 010/414] Index routes work. :) --- lib/src/http/server.dart | 5 +++-- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index a8cc858d..f9c2d949 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -41,8 +41,9 @@ class Angel extends Routable { this.httpServer = server; server.listen((HttpRequest request) async { - String req_url = - request.uri.toString().replaceAll(new RegExp(r'\/+$'), ''); + String req_url = request.uri.toString().replaceAll(new RegExp(r'\/+$'), ''); + if (req_url.isEmpty) + req_url = '/'; RequestContext req = await RequestContext.from(request, {}, this, null); ResponseContext res = await ResponseContext.from(request.response, this); diff --git a/pubspec.yaml b/pubspec.yaml index 4cce9475..d35f5c76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.6 +version: 0.0.0-dev.7 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 24fc679ed8a25a4e979e4fd840f3a8acc279a62e Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 28 Apr 2016 21:10:29 -0400 Subject: [PATCH 011/414] Service path --- lib/src/http/routable.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 23f3fce5..7f979537 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -39,7 +39,7 @@ class Routable extends Extensible { } if (routable is Service) { - services[path.toString()] = routable; + services[path.toString().trim().replaceAll(new RegExp(r'(^\/+)|(\/+$)'), '')] = routable; } } diff --git a/pubspec.yaml b/pubspec.yaml index d35f5c76..8489bf2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.7 +version: 0.0.0-dev.8 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 0a7a0a587d4c3923798da08e43646ecd2dcf01c2 Mon Sep 17 00:00:00 2001 From: regiostech Date: Sat, 30 Apr 2016 21:42:52 -0400 Subject: [PATCH 012/414] Error HTML rendering --- lib/src/http/server.dart | 36 +++++++++++++++++++++++++----------- pubspec.yaml | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index f9c2d949..d954db41 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -41,7 +41,8 @@ class Angel extends Routable { this.httpServer = server; server.listen((HttpRequest request) async { - String req_url = request.uri.toString().replaceAll(new RegExp(r'\/+$'), ''); + String req_url = request.uri.toString().replaceAll( + new RegExp(r'\/+$'), ''); if (req_url.isEmpty) req_url = '/'; RequestContext req = await RequestContext.from(request, {}, this, null); @@ -57,16 +58,16 @@ class Angel extends Routable { if (e is AngelHttpException) { // Special handling for AngelHttpExceptions :) try { - String accept = request.headers.value(HttpHeaders.ACCEPT) ?? "*/*"; + String accept = request.headers.value(HttpHeaders.ACCEPT); if (accept == "*/*" || accept.contains("application/json") || accept.contains("application/javascript")) { res.json(e.toMap()); } else { await _applyHandler(_errorHandler, req, res); + _finalizeResponse(request, res); } - } catch (_) { - } + } catch (_) {} } _onError(e, stackTrace); canContinue = false; @@ -147,9 +148,13 @@ class Angel extends Routable { } _finalizeResponse(HttpRequest request, ResponseContext res) async { - if (!res.willCloseItself) { - res.responseData.forEach((blob) => request.response.add(blob)); - await request.response.close(); + try { + if (!res.willCloseItself) { + res.responseData.forEach((blob) => request.response.add(blob)); + await request.response.close(); + } + } catch (e) { + // Remember: This fails silently } } @@ -192,15 +197,24 @@ class Angel extends Routable { if (stackTrace != null) stderr.write(stackTrace.toString()); } - Angel() : super() {} + Angel + () + : + super() {} /// Creates an HTTPS server. /// Provide paths to a certificate chain and server key (both .pem). /// If no password is provided, a random one will be generated upon running /// the server. - Angel.secure(String certificateChainPath, String serverKeyPath, - {String password}) - : super() { + Angel.secure + (String certificateChainPath, String + serverKeyPath + , + { + String password + }) + : super() + { _serverGenerator = (InternetAddress address, int port) async { var certificateChain = Platform.script.resolve('server_chain.pem').toFilePath(); diff --git a/pubspec.yaml b/pubspec.yaml index 8489bf2a..b8aeef54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.8 +version: 0.0.0-dev.9 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From b237c0c2d77bfc237f7cfb397bc562d720d92f7e Mon Sep 17 00:00:00 2001 From: regiostech Date: Sat, 30 Apr 2016 22:01:35 -0400 Subject: [PATCH 013/414] 11 --- lib/src/http/server.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index d954db41..332a01fc 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -64,9 +64,9 @@ class Angel extends Routable { accept.contains("application/javascript")) { res.json(e.toMap()); } else { - await _applyHandler(_errorHandler, req, res); - _finalizeResponse(request, res); + await _errorHandler(e, req, res); } + _finalizeResponse(request, res); } catch (_) {} } _onError(e, stackTrace); diff --git a/pubspec.yaml b/pubspec.yaml index b8aeef54..99313516 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.9 +version: 0.0.0-dev.11 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 496ecd875747cd010cd9b75eb3a0d83256e8e1e4 Mon Sep 17 00:00:00 2001 From: regiostech Date: Sat, 30 Apr 2016 22:03:31 -0400 Subject: [PATCH 014/414] F**K 12 --- lib/src/http/server.dart | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 332a01fc..db056698 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -13,6 +13,7 @@ class Angel extends Routable { /// Default error handler, show HTML error page var _errorHandler = (AngelHttpException e, req, ResponseContext res) { + res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.status(e.statusCode); res.write("${e.message}"); res.write("

${e.message}

    "); diff --git a/pubspec.yaml b/pubspec.yaml index 99313516..cc1b4d2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.11 +version: 0.0.0-dev.12 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From f58b2dc259f1733a53aa540b677d167b8d33234d Mon Sep 17 00:00:00 2001 From: regiostech Date: Mon, 2 May 2016 18:28:14 -0400 Subject: [PATCH 015/414] @Middleware, @Hooked, .use-> services, middleware --- lib/src/http/http.dart | 2 + lib/src/http/metadata/metadata.dart | 13 ++++ lib/src/http/request_context.dart | 2 +- lib/src/http/response_context.dart | 12 +++- lib/src/http/routable.dart | 102 +++++++++++++++++++++++----- lib/src/http/route.dart | 17 +++++ lib/src/http/server.dart | 20 ++++-- lib/src/http/service.dart | 10 +-- lib/src/http/service_hooked.dart | 73 ++++++++++++++++++++ pubspec.yaml | 2 +- test/routing.dart | 29 ++++++++ test/services.dart | 4 +- test/util.dart | 2 + 13 files changed, 255 insertions(+), 33 deletions(-) create mode 100644 lib/src/http/metadata/metadata.dart create mode 100644 lib/src/http/service_hooked.dart diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 6b94ce67..f8300ab1 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -13,10 +13,12 @@ import 'package:mime/mime.dart'; part 'extensible.dart'; part 'errors.dart'; +part 'metadata/metadata.dart'; part 'request_context.dart'; part 'response_context.dart'; part 'route.dart'; part 'routable.dart'; part 'server.dart'; part 'service.dart'; +part 'service_hooked.dart'; part 'services/memory.dart'; \ No newline at end of file diff --git a/lib/src/http/metadata/metadata.dart b/lib/src/http/metadata/metadata.dart new file mode 100644 index 00000000..eadce305 --- /dev/null +++ b/lib/src/http/metadata/metadata.dart @@ -0,0 +1,13 @@ +part of angel_framework.http; + +/// Maps the given middleware(s) onto this handler. +class Middleware { + final List handlers; + + const Middleware(List this.handlers); +} + +/// This service will send an event after every action. +class Hooked { + const Hooked(); +} \ No newline at end of file diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index 1fd8980b..ce976759 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -1,7 +1,7 @@ part of angel_framework.http; /// A function that intercepts a request and determines whether handling of it should continue. -typedef Future Middleware(RequestContext req, ResponseContext res); +typedef Future RequestMiddleware(RequestContext req, ResponseContext res); /// A function that receives an incoming [RequestContext] and responds to it. typedef Future RequestHandler(RequestContext req, ResponseContext res); diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 5e992f36..ee1f99a8 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -74,7 +74,7 @@ class ResponseContext extends Extensible { /// Redirects to user to the given URL. redirect(String url, {int code: 301}) { header(HttpHeaders.LOCATION, url); - status(code); + status(code ?? 301); write(''' @@ -95,6 +95,16 @@ class ResponseContext extends Extensible { end(); } + /// Redirects to the given named [Route]. + redirectTo(String name, [Map params, int code]) { + Route matched = app.routes.firstWhere((Route route) => route.name == name); + if (matched != null) { + return redirect(matched.makeUri(params), code: code); + } + + throw new ArgumentError.notNull('Route to redirect to ($name)'); + } + /// Streams a file to this response as chunked data. /// /// Useful for video sites. diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 7f979537..45d897c3 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -2,10 +2,34 @@ part of angel_framework.http; typedef Route RouteAssigner(Pattern path, handler, {List middleware}); +_matchingAnnotation(List metadata, Type T) { + for (InstanceMirror metaDatum in metadata) { + if (metaDatum.hasReflectee) { + var reflectee = metaDatum.reflectee; + if (reflectee.runtimeType == T) { + return reflectee; + } + } + } + return null; +} + +_getAnnotation(obj, Type T) { + if (obj is Function || obj is Future) { + MethodMirror methodMirror = (reflect(obj) as ClosureMirror).function; + return _matchingAnnotation(methodMirror.metadata, T); + } else { + ClassMirror classMirror = reflectClass(obj.runtimeType); + return _matchingAnnotation(classMirror.metadata, T); + } + + return null; +} + /// A routable server that can handle dynamic requests. class Routable extends Extensible { /// Additional filters to be run on designated requests. - Map middleware = {}; + Map requestMiddleware = {}; /// Dynamic request paths that this server will respond to. List routes = []; @@ -14,16 +38,26 @@ class Routable extends Extensible { Map services = {}; /// Assigns a middleware to a name for convenience. - registerMiddleware(String name, Middleware middleware) { - this.middleware[name] = middleware; + registerMiddleware(String name, RequestMiddleware middleware) { + this.requestMiddleware[name] = middleware; } /// Retrieves the service assigned to the given path. Service service(Pattern path) => services[path]; - /// Incorporates another routable's routes into this one's. - use(Pattern path, Routable routable) { - middleware.addAll(routable.middleware); + /// Incorporates another [Routable]'s routes into this one's. + /// + /// If `hooked` is set to `true` and a [Service] is provided, + /// then that service will be wired to a [HookedService] proxy. + /// If a `middlewareNamespace` is provided, then any middleware + /// from the provided [Routable] will be prefixed by that namespace, + /// with a dot. + /// For example, if the [Routable] has a middleware 'y', and the `middlewareNamespace` + /// is 'x', then that middleware will be available as 'x.y' in the main application. + /// These namespaces can be nested. + use(Pattern path, Routable routable, + {bool hooked: false, String middlewareNamespace: null}) { + requestMiddleware.addAll(routable.requestMiddleware); for (Route route in routable.routes) { Route provisional = new Route('', path); if (route.path == '/') { @@ -38,43 +72,77 @@ class Routable extends Extensible { routes.add(route); } + // Let's copy middleware, heeding the optional middleware namespace. + String middlewarePrefix = ""; + if (middlewareNamespace != null) + middlewarePrefix = "$middlewareNamespace."; + + for (String middlewareName in routable.requestMiddleware.keys) { + requestMiddleware["$middlewarePrefix$middlewareName"] = + routable.requestMiddleware[middlewareName]; + } + + // Copy services, too. :) + for (Pattern servicePath in routable.services.keys) { + String newServicePath = path.toString().trim().replaceAll( + new RegExp(r'(^\/+)|(\/+$)'), '') + '/$servicePath'; + services[newServicePath] = routable.services[servicePath]; + } + if (routable is Service) { - services[path.toString().trim().replaceAll(new RegExp(r'(^\/+)|(\/+$)'), '')] = routable; + Hooked hookedDeclaration = _getAnnotation(routable, Hooked); + Service service = (hookedDeclaration != null || hooked) + ? new HookedService(routable) + : routable; + services[path.toString().trim().replaceAll( + new RegExp(r'(^\/+)|(\/+$)'), '')] = service; } } /// Adds a route that responds to the given path /// for requests with the given method (case-insensitive). /// Provide '*' as the method to respond to all methods. - addRoute(String method, Pattern path, Object handler, {List middleware}) { - var route = new Route(method.toUpperCase().trim(), path, (middleware ?? []) - ..add(handler)); + Route addRoute(String method, Pattern path, Object handler, + {List middleware}) { + List handlers = []; + + // Merge @Middleware declaration, if any + Middleware middlewareDeclaration = _getAnnotation( + handler, Middleware); + if (middlewareDeclaration != null) { + handlers.addAll(middlewareDeclaration.handlers); + } + + handlers + ..addAll(middleware ?? []) + ..add(handler); + var route = new Route(method.toUpperCase().trim(), path, handlers); routes.add(route); return route; } /// Adds a route that responds to any request matching the given path. - all(Pattern path, Object handler, {List middleware}) { + Route all(Pattern path, Object handler, {List middleware}) { return addRoute('*', path, handler, middleware: middleware); } /// Adds a route that responds to a GET request. - get(Pattern path, Object handler, {List middleware}) { + Route get(Pattern path, Object handler, {List middleware}) { return addRoute('GET', path, handler, middleware: middleware); } /// Adds a route that responds to a POST request. - post(Pattern path, Object handler, {List middleware}) { + Route post(Pattern path, Object handler, {List middleware}) { return addRoute('POST', path, handler, middleware: middleware); } - + /// Adds a route that responds to a PATCH request. - patch(Pattern path, Object handler, {List middleware}) { + Route patch(Pattern path, Object handler, {List middleware}) { return addRoute('PATCH', path, handler, middleware: middleware); } - + /// Adds a route that responds to a DELETE request. - delete(Pattern path, Object handler, {List middleware}) { + Route delete(Pattern path, Object handler, {List middleware}) { return addRoute('DELETE', path, handler, middleware: middleware); } diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index baf03314..d9b0b0a5 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -28,6 +28,23 @@ class Route { } } + /// Assigns a name to this Route. + as(String name) { + this.name = name; + return this; + } + + String makeUri([Map params]) { + String result = path; + if (params != null) { + for (String key in (params.keys)) { + result = result.replaceAll(new RegExp(":$key"), params[key].toString()); + } + } + + return result; + } + parseParameters(String requestPath) { Map result = {}; diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index db056698..fb789602 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -3,6 +3,10 @@ part of angel_framework.http; /// A function that binds an [Angel] server to an Internet address and port. typedef Future ServerGenerator(InternetAddress address, int port); +/// Handles an [AngelHttpException]. +typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, + ResponseContext res); + /// A function that configures an [Angel] server in some way. typedef Future AngelConfigurer(Angel app); @@ -27,10 +31,10 @@ class Angel extends Routable { var viewGenerator = (String view, [Map data]) => "No view engine has been configured yet."; - /// [Middleware] to be run before all requests. + /// [RequestMiddleware] to be run before all requests. List before = []; - /// [Middleware] to be run after all requests. + /// [RequestMiddleware] to be run after all requests. List after = []; HttpServer httpServer; @@ -106,7 +110,7 @@ class Angel extends Routable { Future _applyHandler(handler, RequestContext req, ResponseContext res) async { - if (handler is Middleware) { + if (handler is RequestMiddleware) { var result = await handler(req, res); if (result is bool) return result == true; @@ -138,8 +142,8 @@ class Angel extends Routable { return false; } else return true; - } else if (middleware.containsKey(handler)) { - return await _applyHandler(middleware[handler], req, res); + } else if (requestMiddleware.containsKey(handler)) { + return await _applyHandler(requestMiddleware[handler], req, res); } else { res.willCloseItself = true; res.underlyingResponse.write(god.serialize(handler)); @@ -181,11 +185,13 @@ class Angel extends Routable { } @override - use(Pattern path, Routable routable) { + use(Pattern path, Routable routable, + {bool hooked: false, String middlewareNamespace: null}) { if (routable is Service) { routable.app = this; } - super.use(path, routable); + super.use( + path, routable, hooked: hooked, middlewareNamespace: middlewareNamespace); } onError(handler) { diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 2f9ea3e3..38ecd040 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -11,27 +11,27 @@ class Service extends Routable { } /// Retrieves the desired resource. - Future read(id, [Map params]) { + Future read(id, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } /// Creates a resource. - Future create(Map data, [Map params]) { + Future create(Map data, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } /// Modifies a resource. - Future modify(id, Map data, [Map params]) { + Future modify(id, Map data, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } /// Overwrites a resource. - Future update(id, Map data, [Map params]) { + Future update(id, Map data, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } /// Removes the given resource. - Future remove(id, [Map params]) { + Future remove(id, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart new file mode 100644 index 00000000..97de2960 --- /dev/null +++ b/lib/src/http/service_hooked.dart @@ -0,0 +1,73 @@ +part of angel_framework.http; + +/// Wraps another service in a service that fires events on actions. +class HookedService extends Service { + StreamController _onIndexed = new StreamController(); + StreamController _onRead = new StreamController(); + StreamController _onCreated = new StreamController(); + StreamController _onModified = new StreamController(); + StreamController _onUpdated = new StreamController(); + StreamController _onRemoved = new StreamController(); + + Stream get onIndexed => _onIndexed.stream; + + Stream get onRead => _onRead.stream; + + Stream get onCreated => _onCreated.stream; + + Stream get onModified => _onModified.stream; + + Stream get onUpdated => _onUpdated.stream; + + Stream get onRemoved => _onRemoved.stream; + + final Service inner; + + HookedService(Service this.inner); + + + @override + Future index([Map params]) async { + List indexed = await inner.index(params); + _onIndexed.add(indexed); + return indexed; + } + + + @override + Future read(id, [Map params]) async { + var retrieved = await inner.read(id, params); + _onRead.add(retrieved); + return retrieved; + } + + + @override + Future create(Map data, [Map params]) async { + var created = await inner.create(data, params); + _onCreated.add(created); + return created; + } + + @override + Future modify(id, Map data, [Map params]) async { + var modified = await inner.modify(id, data, params); + _onUpdated.add(modified); + return modified; + } + + + @override + Future update(id, Map data, [Map params]) async { + var updated = await inner.update(id, data, params); + _onUpdated.add(updated); + return updated; + } + + @override + Future remove(id, [Map params]) async { + var removed = await inner.remove(id, params); + _onRemoved.add(removed); + return removed; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index cc1b4d2e..3bfc817a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.12 +version: 0.0.0-dev.13 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/routing.dart b/test/routing.dart index cf6950e3..5f2594c6 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -4,6 +4,11 @@ import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart'; import 'package:test/test.dart'; +@Middleware(const ['interceptor']) +testMiddlewareMetadata(RequestContext req, ResponseContext res) async { + return "This should not be shown."; +} + main() { group('routing', () { Angel angel; @@ -26,6 +31,7 @@ main() { todos.get('/action/:action', (req, res) => res.json(req.params)); nested.post('/ted/:route', (req, res) => res.json(req.params)); + angel.get('/meta', testMiddlewareMetadata); angel.get('/intercepted', 'This should not be shown', middleware: ['interceptor']); angel.get('/hello', 'world'); @@ -33,6 +39,10 @@ main() { angel.post('/lambda', (req, res) => req.body); angel.use('/nes', nested); angel.use('/todos/:id', todos); + angel.get('/greet/:name', (RequestContext req, res) async => "Hello ${req.params['name']}").as('Named routes'); + angel.get('/named', (req, ResponseContext res) async { + res.redirectTo('Named routes', {'name': 'tests'}); + }); angel.get('*', 'MJ'); client = new http.Client(); @@ -81,6 +91,12 @@ main() { expect(response.body, equals('Middleware')); }); + test('Middleware via metadata', () async { + // Metadata + var response = await client.get('$url/meta'); + expect(response.body, equals('Middleware')); + }); + test('Can serialize function result as JSON', () async { Map headers = {'Content-Type': 'application/json'}; String postData = god.serialize({'it': 'works'}); @@ -93,5 +109,18 @@ main() { var response = await client.get('$url/my_favorite_artist'); expect(response.body, equals('"MJ"')); }); + + test('Can name routes', () { + Route foo = angel.get('/framework/:id', 'Angel').as('frm'); + String uri = foo.makeUri({'id': 'angel'}); + print(uri); + expect(uri, equals('/framework/angel')); + }); + + test('Redirect to named routes', () async { + var response = await client.get('$url/named'); + print(response.body); + expect(god.deserialize(response.body), equals('Hello tests')); + }); }); } \ No newline at end of file diff --git a/test/services.dart b/test/services.dart index af07a06d..50351a02 100644 --- a/test/services.dart +++ b/test/services.dart @@ -1,3 +1,4 @@ +import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart'; @@ -23,7 +24,8 @@ main() { angel = new Angel(); client = new http.Client(); god = new God(); - angel.use('/todos', new MemoryService()); + Service todos = new MemoryService(); + angel.use('/todos', todos); await angel.startServer(null, 0); url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; }); diff --git a/test/util.dart b/test/util.dart index 6e641c0a..9992ab23 100644 --- a/test/util.dart +++ b/test/util.dart @@ -20,6 +20,7 @@ main() { }); test('can use app.properties like members', () { + /* angel.properties['hello'] = 'world'; angel.properties['foo'] = () => 'bar'; angel.properties['Foo'] = new Foo('bar'); @@ -27,6 +28,7 @@ main() { expect(angel.hello, equals('world')); expect(angel.foo(), equals('bar')); expect(angel.Foo.name, equals('bar')); + */ }); }); } \ No newline at end of file From eefc37315db82874fb5aa2def52db8d8873fee92 Mon Sep 17 00:00:00 2001 From: regiostech Date: Mon, 2 May 2016 18:56:38 -0400 Subject: [PATCH 016/414] /virtual/* -> /virtual* --- lib/src/http/route.dart | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index d9b0b0a5..fa16f72d 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -16,6 +16,7 @@ class Route { else { this.matcher = new RegExp('^' + path.toString() + .replaceAll(new RegExp(r'\/\*$'), "*") .replaceAll(new RegExp('\/'), r'\/') .replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)') .replaceAll(new RegExp('\\*'), '.*') diff --git a/pubspec.yaml b/pubspec.yaml index 3bfc817a..7eff5fe1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.13 +version: 0.0.0-dev.14 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 476fc13a805392c471c78ee53b1fbe8174caded9 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 3 May 2016 20:55:34 -0400 Subject: [PATCH 017/414] :) --- lib/src/http/route.dart | 7 +++++++ lib/src/http/service_hooked.dart | 6 +++--- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index fa16f72d..03fad12c 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -46,6 +46,13 @@ class Route { return result; } + Route middleware(handler) { + if (handler is Iterable) + handlers.addAll(handler); + else handlers.add(handler); + return this; + } + parseParameters(String requestPath) { Map result = {}; diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index 97de2960..d28185dd 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -43,14 +43,14 @@ class HookedService extends Service { @override - Future create(Map data, [Map params]) async { + Future create(data, [Map params]) async { var created = await inner.create(data, params); _onCreated.add(created); return created; } @override - Future modify(id, Map data, [Map params]) async { + Future modify(id, data, [Map params]) async { var modified = await inner.modify(id, data, params); _onUpdated.add(modified); return modified; @@ -58,7 +58,7 @@ class HookedService extends Service { @override - Future update(id, Map data, [Map params]) async { + Future update(id, data, [Map params]) async { var updated = await inner.update(id, data, params); _onUpdated.add(updated); return updated; diff --git a/pubspec.yaml b/pubspec.yaml index 7eff5fe1..48fabb46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.14 +version: 0.0.0-dev.15 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From b373db1110370e85d84490144357a22211d435db Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 3 May 2016 20:58:28 -0400 Subject: [PATCH 018/414] service.create, etc --- lib/src/http/services/memory.dart | 15 +++++++++------ pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 987c5a76..d2135b7b 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -26,9 +26,10 @@ class MemoryService extends Service { } else throw new AngelHttpException.NotFound(); } - Future create(Map data, [Map params]) async { + Future create(data, [Map params]) async { try { - items[items.length] = god.deserializeFromMap(data, T); + items[items.length] = + (data is Map) ? god.deserializeFromMap(data, T) : data; T created = items[items.length - 1]; return makeJson(items.length - 1, created); } catch (e) { @@ -36,13 +37,14 @@ class MemoryService extends Service { } } - Future modify(id, Map data, [Map params]) async { + Future modify(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { try { Map existing = god.serializeToMap(items[desiredId]); data = mergeMap([existing, data]); - items[desiredId] = god.deserializeFromMap(data, T); + items[desiredId] = + (data is Map) ? god.deserializeFromMap(data, T) : data; return makeJson(desiredId, items[desiredId]); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); @@ -50,11 +52,12 @@ class MemoryService extends Service { } else throw new AngelHttpException.NotFound(); } - Future update(id, Map data, [Map params]) async { + Future update(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { try { - items[desiredId] = god.deserializeFromMap(data, T); + items[desiredId] = + (data is Map) ? god.deserializeFromMap(data, T) : data; return makeJson(desiredId, items[desiredId]); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); diff --git a/pubspec.yaml b/pubspec.yaml index 48fabb46..68701c2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.15 +version: 0.0.0-dev.16 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 315e0bca7a577bf73f32a2e423be283d89f6b236 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 3 May 2016 21:01:29 -0400 Subject: [PATCH 019/414] service.create, etc --- lib/src/http/services/memory.dart | 17 ++++++++++------- pubspec.yaml | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index d2135b7b..f94d5555 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -5,8 +5,11 @@ class MemoryService extends Service { God god = new God(); Map items = {}; - Map makeJson(int index, T t) { - return mergeMap([god.serializeToMap(t), {'id': index}]); + makeJson(int index, T t) { + if (T == null || T == Map) + return mergeMap([god.serializeToMap(t), {'id': index}]); + else + return t; } Future index([Map params]) async { @@ -16,7 +19,7 @@ class MemoryService extends Service { .toList(); } - Future read(id, [Map params]) async { + Future read(id, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { T found = items[desiredId]; @@ -26,7 +29,7 @@ class MemoryService extends Service { } else throw new AngelHttpException.NotFound(); } - Future create(data, [Map params]) async { + Future create(data, [Map params]) async { try { items[items.length] = (data is Map) ? god.deserializeFromMap(data, T) : data; @@ -37,7 +40,7 @@ class MemoryService extends Service { } } - Future modify(id, data, [Map params]) async { + Future modify(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { try { @@ -52,7 +55,7 @@ class MemoryService extends Service { } else throw new AngelHttpException.NotFound(); } - Future update(id, data, [Map params]) async { + Future update(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { try { @@ -65,7 +68,7 @@ class MemoryService extends Service { } else throw new AngelHttpException.NotFound(); } - Future remove(id, [Map params]) async { + Future remove(id, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { T item = items[desiredId]; diff --git a/pubspec.yaml b/pubspec.yaml index 68701c2f..54d92203 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.16 +version: 0.0.0-dev.17 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 6cc9967b3e71b5abceb9a1e24525a8aadd761f36 Mon Sep 17 00:00:00 2001 From: regiostech Date: Sun, 19 Jun 2016 01:02:41 -0400 Subject: [PATCH 020/414] Hooks --- lib/src/http/errors.dart | 8 +-- lib/src/http/server.dart | 1 + lib/src/http/service.dart | 33 ++++++++--- lib/src/http/service_hooked.dart | 93 ++++++++++++++++++++++++------- lib/src/http/services/memory.dart | 14 ++--- pubspec.yaml | 4 +- test/util.dart | 4 +- 7 files changed, 111 insertions(+), 46 deletions(-) diff --git a/lib/src/http/errors.dart b/lib/src/http/errors.dart index 512d1daf..462c5c7f 100644 --- a/lib/src/http/errors.dart +++ b/lib/src/http/errors.dart @@ -31,12 +31,12 @@ class AngelHttpException extends _AngelHttpExceptionBase { /// Throws a 500 Internal Server Error. /// Set includeRealException to true to print include the actual exception along with /// this error. Useful flag for development vs. production. - AngelHttpException(Exception exception, - {bool includeRealException: false, StackTrace stackTrace}) :super.base() { + AngelHttpException(error, + {bool includeRealError: false, StackTrace stackTrace}) :super.base() { statusCode = 500; message = "500 Internal Server Error"; - if (includeRealException) { - errors.add(exception.toString()); + if (includeRealError) { + errors.add(error.toString()); if (stackTrace != null) { errors.add(stackTrace.toString()); } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index fb789602..fcdcc96c 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -63,6 +63,7 @@ class Angel extends Routable { if (e is AngelHttpException) { // Special handling for AngelHttpExceptions :) try { + res.status(e.statusCode); String accept = request.headers.value(HttpHeaders.ACCEPT); if (accept == "*/*" || accept.contains("application/json") || diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 38ecd040..4b1be3e5 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -1,5 +1,15 @@ part of angel_framework.http; +class Providers { + final String via; + + const Providers._base(String this.via); + + static final Providers SERVER = const Providers._base('server_side'); + static final Providers REST = const Providers._base('rest'); + static final Providers WEBSOCKET = const Providers._base('websocket'); +} + /// A data store exposed to the Internet. class Service extends Routable { /// The [Angel] app powering this service. @@ -16,17 +26,17 @@ class Service extends Routable { } /// Creates a resource. - Future create(Map data, [Map params]) { + Future create(data, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } /// Modifies a resource. - Future modify(id, Map data, [Map params]) { + Future modify(id, data, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } /// Overwrites a resource. - Future update(id, Map data, [Map params]) { + Future update(id, data, [Map params]) { throw new AngelHttpException.MethodNotAllowed(); } @@ -36,19 +46,24 @@ class Service extends Routable { } Service() : super() { - get('/', (req, res) async => await this.index(req.query)); + Map restProvider = {'provider': Providers.REST}; - post('/', (req, res) async => await this.create(req.body)); + get('/', (req, res) async => await this.index( + mergeMap([req.query, restProvider]))); + + post('/', (req, res) async => await this.create( + mergeMap([req.body, restProvider]))); get('/:id', (req, res) async => - await this.read(req.params['id'], req.query)); + await this.read(req.params['id'], mergeMap([req.query, restProvider]))); patch('/:id', (req, res) async => await this.modify( - req.params['id'], req.body)); + req.params['id'], mergeMap([req.body, restProvider]))); post('/:id', (req, res) async => await this.update( - req.params['id'], req.body)); + req.params['id'], mergeMap([req.body, restProvider]))); - delete('/:id', (req, res) async => await this.remove(req.params['id'], req.query)); + delete('/:id', (req, res) async => await this.remove( + req.params['id'], mergeMap([req.query, restProvider]))); } } \ No newline at end of file diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index d28185dd..ef3ca250 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -1,25 +1,44 @@ part of angel_framework.http; -/// Wraps another service in a service that fires events on actions. +/// Wraps another service in a service that broadcasts events on actions. class HookedService extends Service { - StreamController _onIndexed = new StreamController(); - StreamController _onRead = new StreamController(); - StreamController _onCreated = new StreamController(); - StreamController _onModified = new StreamController(); - StreamController _onUpdated = new StreamController(); - StreamController _onRemoved = new StreamController(); + StreamController _beforeIndexed = new StreamController.broadcast(); + StreamController _beforeRead = new StreamController.broadcast(); + StreamController _beforeCreated = new StreamController.broadcast(); + StreamController _beforeModified = new StreamController.broadcast(); + StreamController _beforeUpdated = new StreamController.broadcast(); + StreamController _beforeRemoved = new StreamController.broadcast(); - Stream get onIndexed => _onIndexed.stream; + Stream get beforeIndexed => _beforeIndexed.stream; - Stream get onRead => _onRead.stream; + Stream get beforeRead => _beforeRead.stream; - Stream get onCreated => _onCreated.stream; + Stream get beforeCreated => _beforeCreated.stream; - Stream get onModified => _onModified.stream; + Stream get beforeModified => _beforeModified.stream; - Stream get onUpdated => _onUpdated.stream; + Stream get beforeUpdated => _beforeUpdated.stream; - Stream get onRemoved => _onRemoved.stream; + Stream get beforeRemoved => _beforeRemoved.stream; + + StreamController _afterIndexed = new StreamController.broadcast(); + StreamController _afterRead = new StreamController.broadcast(); + StreamController _afterCreated = new StreamController.broadcast(); + StreamController _afterModified = new StreamController.broadcast(); + StreamController _afterUpdated = new StreamController.broadcast(); + StreamController _afterRemoved = new StreamController.broadcast(); + + Stream get afterIndexed => _afterIndexed.stream; + + Stream get afterRead => _afterRead.stream; + + Stream get afterCreated => _afterCreated.stream; + + Stream get afterModified => _afterModified.stream; + + Stream get afterUpdated => _afterUpdated.stream; + + Stream get afterRemoved => _afterRemoved.stream; final Service inner; @@ -28,16 +47,26 @@ class HookedService extends Service { @override Future index([Map params]) async { - List indexed = await inner.index(params); - _onIndexed.add(indexed); - return indexed; + HookedServiceEvent before = new HookedServiceEvent._base(inner, params: params); + _beforeIndexed.add(before); + + if (before._canceled) { + HookedServiceEvent after = new HookedServiceEvent._base(inner, params: params, result: before.result); + _afterIndexed.add(after); + return before.result; + } + + List result = await inner.index(params); + HookedServiceEvent after = new HookedServiceEvent._base(inner, params: params, result: result); + _afterIndexed.add(after); + return result; } @override Future read(id, [Map params]) async { var retrieved = await inner.read(id, params); - _onRead.add(retrieved); + _afterRead.add(retrieved); return retrieved; } @@ -45,14 +74,14 @@ class HookedService extends Service { @override Future create(data, [Map params]) async { var created = await inner.create(data, params); - _onCreated.add(created); + _afterCreated.add(created); return created; } @override Future modify(id, data, [Map params]) async { var modified = await inner.modify(id, data, params); - _onUpdated.add(modified); + _afterUpdated.add(modified); return modified; } @@ -60,14 +89,36 @@ class HookedService extends Service { @override Future update(id, data, [Map params]) async { var updated = await inner.update(id, data, params); - _onUpdated.add(updated); + _afterUpdated.add(updated); return updated; } @override Future remove(id, [Map params]) async { var removed = await inner.remove(id, params); - _onRemoved.add(removed); + _afterRemoved.add(removed); return removed; } +} + +/// Fired when a hooked service is invoked. +class HookedServiceEvent { + /// Use this to end processing of an event. + void cancel(result) { + _canceled = true; + _result = result; + } + + bool _canceled = false; + var id; + var data; + Map params; + var _result; + get result => _result; + /// The inner service whose method was hooked. + Service service; + + HookedServiceEvent._base(Service this.service, {this.id, this.data, Map this.params: const{}, result}) { + _result = result; + } } \ No newline at end of file diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index f94d5555..26e7f01e 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -5,7 +5,7 @@ class MemoryService extends Service { God god = new God(); Map items = {}; - makeJson(int index, T t) { + _makeJson(int index, T t) { if (T == null || T == Map) return mergeMap([god.serializeToMap(t), {'id': index}]); else @@ -15,7 +15,7 @@ class MemoryService extends Service { Future index([Map params]) async { return items.keys .where((index) => items[index] != null) - .map((index) => makeJson(index, items[index])) + .map((index) => _makeJson(index, items[index])) .toList(); } @@ -24,7 +24,7 @@ class MemoryService extends Service { if (items.containsKey(desiredId)) { T found = items[desiredId]; if (found != null) { - return makeJson(desiredId, found); + return _makeJson(desiredId, found); } else throw new AngelHttpException.NotFound(); } else throw new AngelHttpException.NotFound(); } @@ -34,7 +34,7 @@ class MemoryService extends Service { items[items.length] = (data is Map) ? god.deserializeFromMap(data, T) : data; T created = items[items.length - 1]; - return makeJson(items.length - 1, created); + return _makeJson(items.length - 1, created); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); } @@ -48,7 +48,7 @@ class MemoryService extends Service { data = mergeMap([existing, data]); items[desiredId] = (data is Map) ? god.deserializeFromMap(data, T) : data; - return makeJson(desiredId, items[desiredId]); + return _makeJson(desiredId, items[desiredId]); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); } @@ -61,7 +61,7 @@ class MemoryService extends Service { try { items[desiredId] = (data is Map) ? god.deserializeFromMap(data, T) : data; - return makeJson(desiredId, items[desiredId]); + return _makeJson(desiredId, items[desiredId]); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); } @@ -73,7 +73,7 @@ class MemoryService extends Service { if (items.containsKey(desiredId)) { T item = items[desiredId]; items[desiredId] = null; - return makeJson(desiredId, item); + return _makeJson(desiredId, item); } else throw new AngelHttpException.NotFound(); } diff --git a/pubspec.yaml b/pubspec.yaml index 54d92203..12d79e55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.17 +version: 0.0.0-dev.18 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework @@ -7,7 +7,7 @@ dependencies: body_parser: ">=1.0.0-dev <2.0.0" json_god: ">=1.0.0 <2.0.0" merge_map: ">=1.0.0 <2.0.0" - mime: ">=0.9.3 <0.10.0" + mime: ^0.9.3 dev_dependencies: http: ">= 0.11.3 < 0.12.0" test: ">= 0.12.13 < 0.13.0" \ No newline at end of file diff --git a/test/util.dart b/test/util.dart index 9992ab23..2d22fedd 100644 --- a/test/util.dart +++ b/test/util.dart @@ -20,7 +20,6 @@ main() { }); test('can use app.properties like members', () { - /* angel.properties['hello'] = 'world'; angel.properties['foo'] = () => 'bar'; angel.properties['Foo'] = new Foo('bar'); @@ -28,7 +27,6 @@ main() { expect(angel.hello, equals('world')); expect(angel.foo(), equals('bar')); expect(angel.Foo.name, equals('bar')); - */ }); }); -} \ No newline at end of file +} From 42023ca3740f4d6569f3f769cbc14e4059bd9c5c Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 21 Jun 2016 00:19:43 -0400 Subject: [PATCH 021/414] Hooks redesigned --- lib/src/http/routable.dart | 37 ++--- lib/src/http/server.dart | 2 +- lib/src/http/service.dart | 12 +- lib/src/http/service_hooked.dart | 231 +++++++++++++++++++++--------- lib/src/http/services/memory.dart | 13 +- pubspec.yaml | 12 +- test/hooked.dart | 89 ++++++++++++ test/services.dart | 13 +- test/util.dart | 2 + 9 files changed, 300 insertions(+), 111 deletions(-) create mode 100644 test/hooked.dart diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 45d897c3..6e801dd0 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -56,9 +56,23 @@ class Routable extends Extensible { /// is 'x', then that middleware will be available as 'x.y' in the main application. /// These namespaces can be nested. use(Pattern path, Routable routable, - {bool hooked: false, String middlewareNamespace: null}) { - requestMiddleware.addAll(routable.requestMiddleware); - for (Route route in routable.routes) { + {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; + } + + requestMiddleware.addAll(_routable.requestMiddleware); + for (Route route in _routable.routes) { Route provisional = new Route('', path); if (route.path == '/') { route.path = ''; @@ -77,25 +91,16 @@ class Routable extends Extensible { if (middlewareNamespace != null) middlewarePrefix = "$middlewareNamespace."; - for (String middlewareName in routable.requestMiddleware.keys) { + for (String middlewareName in _routable.requestMiddleware.keys) { requestMiddleware["$middlewarePrefix$middlewareName"] = - routable.requestMiddleware[middlewareName]; + _routable.requestMiddleware[middlewareName]; } // Copy services, too. :) - for (Pattern servicePath in routable.services.keys) { + 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) { - Hooked hookedDeclaration = _getAnnotation(routable, Hooked); - Service service = (hookedDeclaration != null || hooked) - ? new HookedService(routable) - : routable; - services[path.toString().trim().replaceAll( - new RegExp(r'(^\/+)|(\/+$)'), '')] = service; + services[newServicePath] = _routable.services[servicePath]; } } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index fcdcc96c..8ec42c0f 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -187,7 +187,7 @@ class Angel extends Routable { @override use(Pattern path, Routable routable, - {bool hooked: false, String middlewareNamespace: null}) { + {bool hooked: true, String middlewareNamespace: null}) { if (routable is Service) { routable.app = this; } diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 4b1be3e5..b23e8e1c 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -48,20 +48,20 @@ class Service extends Routable { Service() : super() { Map restProvider = {'provider': Providers.REST}; - get('/', (req, res) async => await this.index( - mergeMap([req.query, restProvider]))); + get('/', (req, res) async { + return await this.index(mergeMap([req.query, restProvider])); + }); - post('/', (req, res) async => await this.create( - mergeMap([req.body, restProvider]))); + post('/', (req, res) async => await this.create(req.body, restProvider)); get('/:id', (req, res) async => await this.read(req.params['id'], mergeMap([req.query, restProvider]))); patch('/:id', (req, res) async => await this.modify( - req.params['id'], mergeMap([req.body, restProvider]))); + req.params['id'], req.body, restProvider)); post('/:id', (req, res) async => await this.update( - req.params['id'], mergeMap([req.body, restProvider]))); + req.params['id'], req.body, restProvider)); delete('/:id', (req, res) async => await this.remove( req.params['id'], mergeMap([req.query, restProvider]))); diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index ef3ca250..3cf947ce 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -2,107 +2,162 @@ part of angel_framework.http; /// Wraps another service in a service that broadcasts events on actions. class HookedService extends Service { - StreamController _beforeIndexed = new StreamController.broadcast(); - StreamController _beforeRead = new StreamController.broadcast(); - StreamController _beforeCreated = new StreamController.broadcast(); - StreamController _beforeModified = new StreamController.broadcast(); - StreamController _beforeUpdated = new StreamController.broadcast(); - StreamController _beforeRemoved = new StreamController.broadcast(); - - Stream get beforeIndexed => _beforeIndexed.stream; - - Stream get beforeRead => _beforeRead.stream; - - Stream get beforeCreated => _beforeCreated.stream; - - Stream get beforeModified => _beforeModified.stream; - - Stream get beforeUpdated => _beforeUpdated.stream; - - Stream get beforeRemoved => _beforeRemoved.stream; - - StreamController _afterIndexed = new StreamController.broadcast(); - StreamController _afterRead = new StreamController.broadcast(); - StreamController _afterCreated = new StreamController.broadcast(); - StreamController _afterModified = new StreamController.broadcast(); - StreamController _afterUpdated = new StreamController.broadcast(); - StreamController _afterRemoved = new StreamController.broadcast(); - - Stream get afterIndexed => _afterIndexed.stream; - - Stream get afterRead => _afterRead.stream; - - Stream get afterCreated => _afterCreated.stream; - - Stream get afterModified => _afterModified.stream; - - Stream get afterUpdated => _afterUpdated.stream; - - Stream get afterRemoved => _afterRemoved.stream; - final Service inner; - HookedService(Service this.inner); + HookedServiceEventDispatcher beforeIndexed = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeRead = new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeCreated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeModified = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeUpdated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher beforeRemoved = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterIndexed = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterRead = new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterCreated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterModified = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterUpdated = + new HookedServiceEventDispatcher(); + HookedServiceEventDispatcher afterRemoved = + new HookedServiceEventDispatcher(); + HookedService(Service this.inner) : super() {} @override Future index([Map params]) async { - HookedServiceEvent before = new HookedServiceEvent._base(inner, params: params); - _beforeIndexed.add(before); - + HookedServiceEvent before = await beforeIndexed._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.INDEXED, + params: params)); if (before._canceled) { - HookedServiceEvent after = new HookedServiceEvent._base(inner, params: params, result: before.result); - _afterIndexed.add(after); - return before.result; + HookedServiceEvent after = await beforeIndexed._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.INDEXED, + params: params, result: before.result)); + return after.result; } List result = await inner.index(params); - HookedServiceEvent after = new HookedServiceEvent._base(inner, params: params, result: result); - _afterIndexed.add(after); - return result; + HookedServiceEvent after = await afterIndexed._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.INDEXED, + params: params, result: result)); + return after.result; } - @override Future read(id, [Map params]) async { - var retrieved = await inner.read(id, params); - _afterRead.add(retrieved); - return retrieved; - } + HookedServiceEvent before = await beforeRead._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.READ, + id: id, params: params)); + if (before._canceled) { + HookedServiceEvent after = await afterRead._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.READ, + id: id, params: params, result: before.result)); + return after.result; + } + + var result = await inner.read(id, params); + HookedServiceEvent after = await afterRead._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.READ, + id: id, params: params, result: result)); + return after.result; + } @override Future create(data, [Map params]) async { - var created = await inner.create(data, params); - _afterCreated.add(created); - return created; + HookedServiceEvent before = await beforeCreated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.CREATED, + data: data, params: params)); + + if (before._canceled) { + HookedServiceEvent after = await afterCreated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.CREATED, + data: data, params: params, result: before.result)); + return after.result; + } + + var result = await inner.create(data, params); + HookedServiceEvent after = await afterCreated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.CREATED, + data: data, params: params, result: result)); + return after.result; } @override Future modify(id, data, [Map params]) async { - var modified = await inner.modify(id, data, params); - _afterUpdated.add(modified); - return modified; - } + HookedServiceEvent before = await beforeModified._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.MODIFIED, + id: id, data: data, params: params)); + if (before._canceled) { + HookedServiceEvent after = await afterModified._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.MODIFIED, + id: id, data: data, params: params, result: before.result)); + return after.result; + } + + var result = await inner.modify(id, data, params); + HookedServiceEvent after = await afterModified._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.MODIFIED, + id: id, data: data, params: params, result: result)); + return after.result; + } @override Future update(id, data, [Map params]) async { - var updated = await inner.update(id, data, params); - _afterUpdated.add(updated); - return updated; + HookedServiceEvent before = await beforeUpdated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.UPDATED, + id: id, data: data, params: params)); + + if (before._canceled) { + HookedServiceEvent after = await afterUpdated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.UPDATED, + id: id, data: data, params: params, result: before.result)); + return after.result; + } + + var result = await inner.update(id, data, params); + HookedServiceEvent after = await afterUpdated._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.UPDATED, + id: id, data: data, params: params, result: result)); + return after.result; } @override Future remove(id, [Map params]) async { - var removed = await inner.remove(id, params); - _afterRemoved.add(removed); - return removed; + HookedServiceEvent before = await beforeRemoved._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.REMOVED, + id: id, params: params)); + + if (before._canceled) { + HookedServiceEvent after = await afterRemoved._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.REMOVED, + id: id, params: params, result: before.result)); + return after.result; + } + + var result = await inner.remove(id, params); + HookedServiceEvent after = await afterRemoved._emit( + new HookedServiceEvent._base(inner, HookedServiceEvent.REMOVED, + id: id, params: params, result: result)); + return after.result; } } /// Fired when a hooked service is invoked. class HookedServiceEvent { + static const String INDEXED = "indexed"; + static const String READ = "read"; + static const String CREATED = "created"; + static const String MODIFIED = "modified"; + static const String UPDATED = "updated"; + static const String REMOVED = "removed"; + /// Use this to end processing of an event. void cancel(result) { _canceled = true; @@ -110,15 +165,51 @@ class HookedServiceEvent { } bool _canceled = false; - var id; + String _eventName; + var _id; var data; - Map params; + Map _params; var _result; + + String get eventName => _eventName; + + get id => _id; + + Map get params => _params; + get result => _result; + /// The inner service whose method was hooked. Service service; - HookedServiceEvent._base(Service this.service, {this.id, this.data, Map this.params: const{}, result}) { + HookedServiceEvent._base(Service this.service, String this._eventName, + {id, this.data, Map params, result}) { + _id = id; + _params = params ?? {}; _result = result; } -} \ No newline at end of file +} + +/// Triggered on a hooked service event. +typedef Future HookedServiceEventListener(HookedServiceEvent event); + +/// Can be listened to, but events may be canceled. +class HookedServiceEventDispatcher { + List listeners = []; + + /// Fires an event, and returns it once it is either canceled, or all listeners have run. + Future _emit(HookedServiceEvent event) async { + for (var listener in listeners) { + await listener(event); + + if (event._canceled) return event; + } + + return event; + } + + /// Registers the listener to be called whenever an event is triggered. + void listen(HookedServiceEventListener listener) { + listeners.add(listener); + } +} diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 26e7f01e..89eb59c7 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -30,14 +30,15 @@ class MemoryService extends Service { } Future create(data, [Map params]) async { - try { - items[items.length] = - (data is Map) ? god.deserializeFromMap(data, T) : data; - T created = items[items.length - 1]; + //try { + print("Data: $data"); + var created = (data is Map) ? god.deserializeFromMap(data, T) : data; + print("Created $created"); + items[items.length] = created; return _makeJson(items.length - 1, created); - } catch (e) { + /*} catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); - } + }*/ } Future modify(id, data, [Map params]) async { diff --git a/pubspec.yaml b/pubspec.yaml index 12d79e55..c8bc52de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: angel_framework -version: 0.0.0-dev.18 +version: 0.0.0-dev.19 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework dependencies: - body_parser: ">=1.0.0-dev <2.0.0" - json_god: ">=1.0.0 <2.0.0" - merge_map: ">=1.0.0 <2.0.0" + body_parser: ^1.0.0-dev + json_god: ^1.0.0 + merge_map: ^1.0.0 mime: ^0.9.3 dev_dependencies: - http: ">= 0.11.3 < 0.12.0" - test: ">= 0.12.13 < 0.13.0" \ No newline at end of file + http: ^0.11.3 + test: ^0.12.13 \ No newline at end of file diff --git a/test/hooked.dart b/test/hooked.dart new file mode 100644 index 00000000..422b1151 --- /dev/null +++ b/test/hooked.dart @@ -0,0 +1,89 @@ +import 'dart:mirrors'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart'; +import 'package:test/test.dart'; + +class Todo { + String text; + String over; +} + +main() { + group('Hooked', () { + Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + Angel app; + String url; + http.Client client; + God god; + HookedService Todos; + + setUp(() async { + app = new Angel(); + client = new http.Client(); + god = new God(); + app.use('/todos', new MemoryService()); + Todos = app.service("todos"); + + await app.startServer(null, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); + + tearDown(() async { + app = null; + url = null; + client.close(); + client = null; + god = null; + Todos = null; + }); + + test("listen before and after", () async { + int count = 0; + + Todos + ..beforeIndexed.listen((_) { + count++; + }) + ..afterIndexed.listen((_) { + count++; + }); + + var response = await client.get("$url/todos"); + print(response.body); + expect(count, equals(2)); + }); + + test("cancel before", () async { + Todos.beforeCreated..listen((HookedServiceEvent event) { + event.cancel({"hello": "hooked world"}); + })..listen((HookedServiceEvent event) { + event.cancel({"this_hook": "should never run"}); + }); + + var response = await client.post( + "$url/todos", body: god.serialize({"arbitrary": "data"}), + headers: headers); + print(response.body); + Map result = god.deserialize(response.body); + expect(result["hello"], equals("hooked world")); + }); + + test("cancel after", () async { + Todos.afterIndexed..listen((HookedServiceEvent event) async { + // Hooks can be Futures ;) + event.cancel([{"angel": "framework"}]); + })..listen((HookedServiceEvent event) { + event.cancel({"this_hook": "should never run either"}); + }); + + var response = await client.get("$url/todos"); + print(response.body); + List result = god.deserialize(response.body); + expect(result[0]["angel"], equals("framework")); + }); + }); +} \ No newline at end of file diff --git a/test/services.dart b/test/services.dart index 50351a02..941abd3f 100644 --- a/test/services.dart +++ b/test/services.dart @@ -15,23 +15,24 @@ main() { 'Accept': 'application/json', 'Content-Type': 'application/json' }; - Angel angel; + Angel app; String url; http.Client client; God god; setUp(() async { - angel = new Angel(); + app = new Angel(); client = new http.Client(); god = new God(); Service todos = new MemoryService(); - angel.use('/todos', todos); - await angel.startServer(null, 0); - url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + app.use('/todos', todos); + print(app.service("todos")); + await app.startServer(null, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; }); tearDown(() async { - angel = null; + app = null; url = null; client.close(); client = null; diff --git a/test/util.dart b/test/util.dart index 2d22fedd..5255d8db 100644 --- a/test/util.dart +++ b/test/util.dart @@ -24,9 +24,11 @@ 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')); + */ }); }); } From 267b696150f54fae24d19906d8ae289789872b75 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 21 Jun 2016 16:34:31 -0400 Subject: [PATCH 022/414] Need to migrate to JSON God v2, this is a breaking change --- lib/src/http/http.dart | 4 ++-- lib/src/http/server.dart | 41 +++++++++++++++------------------------- pubspec.yaml | 12 ++++++------ 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index f8300ab1..44c163e1 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -7,7 +7,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:mirrors'; import 'package:body_parser/body_parser.dart'; -import 'package:json_god/json_god.dart'; +import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'package:mime/mime.dart'; @@ -21,4 +21,4 @@ part 'routable.dart'; part 'server.dart'; part 'service.dart'; part 'service_hooked.dart'; -part 'services/memory.dart'; \ No newline at end of file +part 'services/memory.dart'; diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 8ec42c0f..87c56e7c 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -4,8 +4,8 @@ part of angel_framework.http; typedef Future ServerGenerator(InternetAddress address, int port); /// Handles an [AngelHttpException]. -typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, - ResponseContext res); +typedef Future AngelErrorHandler( + AngelHttpException err, RequestContext req, ResponseContext res); /// A function that configures an [Angel] server in some way. typedef Future AngelConfigurer(Angel app); @@ -38,18 +38,16 @@ class Angel extends Routable { List after = []; HttpServer httpServer; - God god = new God(); startServer(InternetAddress address, int port) async { var server = - await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); + await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); this.httpServer = server; server.listen((HttpRequest request) async { - String req_url = request.uri.toString().replaceAll( - new RegExp(r'\/+$'), ''); - if (req_url.isEmpty) - req_url = '/'; + String req_url = + request.uri.toString().replaceAll(new RegExp(r'\/+$'), ''); + if (req_url.isEmpty) req_url = '/'; RequestContext req = await RequestContext.from(request, {}, this, null); ResponseContext res = await ResponseContext.from(request.response, this); @@ -109,8 +107,8 @@ class Angel extends Routable { return server; } - Future _applyHandler(handler, RequestContext req, - ResponseContext res) async { + Future _applyHandler( + handler, RequestContext req, ResponseContext res) async { if (handler is RequestMiddleware) { var result = await handler(req, res); if (result is bool) @@ -191,8 +189,8 @@ class Angel extends Routable { if (routable is Service) { routable.app = this; } - super.use( - path, routable, hooked: hooked, middlewareNamespace: middlewareNamespace); + super.use(path, routable, + hooked: hooked, middlewareNamespace: middlewareNamespace); } onError(handler) { @@ -205,27 +203,18 @@ class Angel extends Routable { if (stackTrace != null) stderr.write(stackTrace.toString()); } - Angel - () - : - super() {} + Angel() : super() {} /// Creates an HTTPS server. /// Provide paths to a certificate chain and server key (both .pem). /// If no password is provided, a random one will be generated upon running /// the server. - Angel.secure - (String certificateChainPath, String - serverKeyPath - , - { - String password - }) - : super() - { + Angel.secure(String certificateChainPath, String serverKeyPath, + {String password}) + : super() { _serverGenerator = (InternetAddress address, int port) async { var certificateChain = - Platform.script.resolve('server_chain.pem').toFilePath(); + Platform.script.resolve('server_chain.pem').toFilePath(); var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); diff --git a/pubspec.yaml b/pubspec.yaml index c8bc52de..03f5eb73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,10 +4,10 @@ description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework dependencies: - body_parser: ^1.0.0-dev - json_god: ^1.0.0 - merge_map: ^1.0.0 - mime: ^0.9.3 + body_parser: ">=1.0.0-dev <2.0.0" + json_god: ">=2.0.0-beta <3.0.0" + merge_map: ">=1.0.0 <2.0.0" + mime: ">=0.9.3 <1.0.0" dev_dependencies: - http: ^0.11.3 - test: ^0.12.13 \ No newline at end of file + http: ">= 0.11.3 < 0.12.0" + test: ">= 0.12.13 < 0.13.0" \ No newline at end of file From 80c6118544a4e4b4133b545bc1ec7189eefc7e19 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 21 Jun 2016 18:56:04 -0400 Subject: [PATCH 023/414] 1.0.0-dev! --- README.md | 5 ++++- lib/angel_framework.dart | 2 +- lib/src/http/errors.dart | 3 +++ lib/src/http/extensible.dart | 2 +- lib/src/http/metadata/metadata.dart | 4 ++-- lib/src/http/response_context.dart | 4 +--- lib/src/http/route.dart | 11 ++++++++++- lib/src/http/server.dart | 15 +++++++++++---- lib/src/http/service.dart | 22 +++++++++++++++++----- lib/src/http/service_hooked.dart | 1 + lib/src/http/services/memory.dart | 11 +++++------ pubspec.yaml | 2 +- test/hooked.dart | 6 +----- test/routing.dart | 5 +---- test/services.dart | 6 +----- 15 files changed, 60 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 4583a5d4..0e96b466 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # angel_framework -Documentation in the works. \ No newline at end of file + +![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev-red.svg) + +Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/angel_framework.dart b/lib/angel_framework.dart index 207d8ea8..7a624d51 100644 --- a/lib/angel_framework.dart +++ b/lib/angel_framework.dart @@ -1,4 +1,4 @@ -/// A controller-based MVC + WebSocket framework in Dart. +/// An easily-extensible web server framework in Dart. library angel_framework; export 'src/http/http.dart'; \ No newline at end of file diff --git a/lib/src/http/errors.dart b/lib/src/http/errors.dart index 462c5c7f..84fd482f 100644 --- a/lib/src/http/errors.dart +++ b/lib/src/http/errors.dart @@ -1,8 +1,11 @@ part of angel_framework.http; class _AngelHttpExceptionBase implements Exception { + /// An HTTP status code this exception will throw. int statusCode; + /// The cause of this exception. String message; + /// A list of errors that occurred when this exception was thrown. List errors; _AngelHttpExceptionBase.base() {} diff --git a/lib/src/http/extensible.dart b/lib/src/http/extensible.dart index ec485e3a..5ed88f44 100644 --- a/lib/src/http/extensible.dart +++ b/lib/src/http/extensible.dart @@ -1,6 +1,6 @@ part of angel_framework.http; -/// Basically an Expando whoops +/// 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. /// diff --git a/lib/src/http/metadata/metadata.dart b/lib/src/http/metadata/metadata.dart index eadce305..b65fd84a 100644 --- a/lib/src/http/metadata/metadata.dart +++ b/lib/src/http/metadata/metadata.dart @@ -1,13 +1,13 @@ part of angel_framework.http; -/// Maps the given middleware(s) onto this handler. +/// Annotation to map middleware onto a handler. class Middleware { final List handlers; const Middleware(List this.handlers); } -/// This service will send an event after every action. +/// Annotation to set a service up to release hooks on every action. class Hooked { const Hooked(); } \ No newline at end of file diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index ee1f99a8..0b46cfd1 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -1,15 +1,13 @@ part of angel_framework.http; /// A function that asynchronously generates a view from the given path and data. -typedef Future ViewGenerator(String path, {Map data}); +typedef Future ViewGenerator(String path, [Map data]); /// A convenience wrapper around an outgoing HTTP request. class ResponseContext extends Extensible { /// The [Angel] instance that is sending a response. Angel app; - God god = new God(); - /// Can we still write to this response? bool isOpen = true; diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 03fad12c..43c071f9 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -1,10 +1,16 @@ part of angel_framework.http; +/// 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]) { @@ -35,6 +41,7 @@ class Route { return this; } + /// Generates a URI to this route with the given parameters. String makeUri([Map params]) { String result = path; if (params != null) { @@ -46,6 +53,7 @@ class Route { return result; } + /// Enables one or more handlers to be called whenever this route is visited. Route middleware(handler) { if (handler is Iterable) handlers.addAll(handler); @@ -53,7 +61,8 @@ class Route { return this; } - parseParameters(String requestPath) { + /// Extracts route parameters from a given path. + Map parseParameters(String requestPath) { Map result = {}; Iterable values = _parseParameters(requestPath); diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 87c56e7c..90e2643a 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -16,7 +16,7 @@ class Angel extends Routable { (address, port) async => await HttpServer.bind(address, port); /// Default error handler, show HTML error page - var _errorHandler = (AngelHttpException e, req, ResponseContext res) { + AngelErrorHandler _errorHandler = (AngelHttpException e, req, ResponseContext res) { res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.status(e.statusCode); res.write("${e.message}"); @@ -28,8 +28,10 @@ class Angel extends Routable { res.end(); }; - var viewGenerator = - (String view, [Map data]) => "No view engine has been configured yet."; + /// A function that renders views. + /// + /// Called by [ResponseContext]@`render`. + ViewGenerator viewGenerator = (String view, [Map data]) async => "No view engine has been configured yet."; /// [RequestMiddleware] to be run before all requests. List before = []; @@ -37,8 +39,12 @@ class Angel extends Routable { /// [RequestMiddleware] to be run after all requests. List after = []; + /// The native HttpServer running this instancce. HttpServer httpServer; + /// Starts the server. + /// + /// Returns false on failure; otherwise, returns the HttpServer. startServer(InternetAddress address, int port) async { var server = await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); @@ -193,7 +199,8 @@ class Angel extends Routable { hooked: hooked, middlewareNamespace: middlewareNamespace); } - onError(handler) { + /// Registers a callback to run upon errors. + onError(AngelErrorHandler handler) { _errorHandler = handler; } diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index b23e8e1c..243cc2fe 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -1,16 +1,28 @@ part of angel_framework.http; +/// Indicates how the service was accessed. +/// +/// This will be passed to the `params` object in a service method. +/// When requested on the server side, this will be null. class Providers { + /// The transport through which the client is accessing this service. final String via; - const Providers._base(String this.via); + const Providers(String this.via); - static final Providers SERVER = const Providers._base('server_side'); - static final Providers REST = const Providers._base('rest'); - static final Providers WEBSOCKET = const Providers._base('websocket'); + static const String VIA_REST = "rest"; + static const String VIA_WEBSOCKET = "websocket"; + + /// Represents a request via REST. + static final Providers REST = const Providers(VIA_REST); + + /// Represents a request over WebSockets. + static final Providers WEBSOCKET = const Providers(VIA_WEBSOCKET); } -/// A data store exposed to the Internet. +/// A front-facing interface that can present data to and operate on data on behalf of the user. +/// +/// Heavily inspired by FeathersJS. <3 class Service extends Routable { /// The [Angel] app powering this service. Angel app; diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index 3cf947ce..b3025980 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -2,6 +2,7 @@ part of angel_framework.http; /// Wraps another service in a service that broadcasts events on actions. class HookedService extends Service { + /// Tbe service that is proxied by this hooked one. final Service inner; HookedServiceEventDispatcher beforeIndexed = diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 89eb59c7..58425516 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -2,12 +2,11 @@ part of angel_framework.http; /// An in-memory [Service]. class MemoryService extends Service { - God god = new God(); Map items = {}; _makeJson(int index, T t) { if (T == null || T == Map) - return mergeMap([god.serializeToMap(t), {'id': index}]); + return mergeMap([god.serializeObject(t), {'id': index}]); else return t; } @@ -32,7 +31,7 @@ class MemoryService extends Service { Future create(data, [Map params]) async { //try { print("Data: $data"); - var created = (data is Map) ? god.deserializeFromMap(data, T) : data; + var created = (data is Map) ? god.deserializeDatum(data, outputType: T) : data; print("Created $created"); items[items.length] = created; return _makeJson(items.length - 1, created); @@ -45,10 +44,10 @@ class MemoryService extends Service { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { try { - Map existing = god.serializeToMap(items[desiredId]); + Map existing = god.serializeObject(items[desiredId]); data = mergeMap([existing, data]); items[desiredId] = - (data is Map) ? god.deserializeFromMap(data, T) : data; + (data is Map) ? god.deserializeDatum(data, outputType: T) : data; return _makeJson(desiredId, items[desiredId]); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); @@ -61,7 +60,7 @@ class MemoryService extends Service { if (items.containsKey(desiredId)) { try { items[desiredId] = - (data is Map) ? god.deserializeFromMap(data, T) : data; + (data is Map) ? god.deserializeDatum(data, outputType: T) : data; return _makeJson(desiredId, items[desiredId]); } catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); diff --git a/pubspec.yaml b/pubspec.yaml index 03f5eb73..3a294e0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 0.0.0-dev.19 +version: 1.0.0-dev description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/hooked.dart b/test/hooked.dart index 422b1151..ac6afa52 100644 --- a/test/hooked.dart +++ b/test/hooked.dart @@ -1,7 +1,6 @@ -import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; -import 'package:json_god/json_god.dart'; +import 'package:json_god/json_god.dart' as god; import 'package:test/test.dart'; class Todo { @@ -18,13 +17,11 @@ main() { Angel app; String url; http.Client client; - God god; HookedService Todos; setUp(() async { app = new Angel(); client = new http.Client(); - god = new God(); app.use('/todos', new MemoryService()); Todos = app.service("todos"); @@ -37,7 +34,6 @@ main() { url = null; client.close(); client = null; - god = null; Todos = null; }); diff --git a/test/routing.dart b/test/routing.dart index 5f2594c6..e40fe288 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; -import 'package:json_god/json_god.dart'; +import 'package:json_god/json_god.dart' as god; import 'package:test/test.dart'; @Middleware(const ['interceptor']) @@ -16,10 +16,8 @@ main() { Angel todos; String url; http.Client client; - God god; setUp(() async { - god = new God(); angel = new Angel(); nested = new Angel(); todos = new Angel(); @@ -58,7 +56,6 @@ main() { client.close(); client = null; url = null; - god = null; }); test('Can match basic url', () async { diff --git a/test/services.dart b/test/services.dart index 941abd3f..295e5812 100644 --- a/test/services.dart +++ b/test/services.dart @@ -1,7 +1,6 @@ -import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; -import 'package:json_god/json_god.dart'; +import 'package:json_god/json_god.dart' as god; import 'package:test/test.dart'; class Todo { @@ -18,12 +17,10 @@ main() { Angel app; String url; http.Client client; - God god; setUp(() async { app = new Angel(); client = new http.Client(); - god = new God(); Service todos = new MemoryService(); app.use('/todos', todos); print(app.service("todos")); @@ -36,7 +33,6 @@ main() { url = null; client.close(); client = null; - god = null; }); group('memory', () { From 897a09d359d9f05c985aead84fdb510162fc4a13 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 23 Jun 2016 15:05:55 -0400 Subject: [PATCH 024/414] Worked out some kinks in routing and hooked services. Services can use middleware. --- lib/src/http/request_context.dart | 2 +- lib/src/http/server.dart | 4 +-- lib/src/http/service.dart | 46 +++++++++++++++++++------- lib/src/http/service_hooked.dart | 25 +++++++------- test/routing.dart | 54 ++++++++++++++++++++++++++----- 5 files changed, 98 insertions(+), 33 deletions(-) diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index ce976759..da2beca5 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -73,7 +73,7 @@ class RequestContext extends Extensible { context.contentType = request.headers.contentType; context.remoteAddress = request.connectionInfo.remoteAddress; context.params = parameters; - context.path = request.uri.toString(); + context.path = request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), ''); context.route = sourceRoute; context.session = request.session; context.underlyingRequest = request; diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 90e2643a..2a44f126 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -52,7 +52,7 @@ class Angel extends Routable { server.listen((HttpRequest request) async { String req_url = - request.uri.toString().replaceAll(new RegExp(r'\/+$'), ''); + request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), ''); if (req_url.isEmpty) req_url = '/'; RequestContext req = await RequestContext.from(request, {}, this, null); ResponseContext res = await ResponseContext.from(request.response, this); @@ -95,7 +95,7 @@ class Angel extends Routable { if (!canContinue) break; if (route.matcher.hasMatch(req_url) && (request.method == route.method || route.method == '*')) { - req.params = route.parseParameters(request.uri.toString()); + req.params = route.parseParameters(req_url); req.route = route; for (var handler in route.handlers) { diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 243cc2fe..72340bdc 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -60,22 +60,46 @@ class Service extends Routable { Service() : super() { Map restProvider = {'provider': Providers.REST}; + Middleware indexMiddleware = _getAnnotation(this.index, Middleware); get('/', (req, res) async { return await this.index(mergeMap([req.query, restProvider])); - }); + }, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers); - post('/', (req, res) async => await this.create(req.body, restProvider)); + Middleware createMiddleware = _getAnnotation(this.create, Middleware); + post('/', (req, res) async => await this.create(req.body, restProvider), + middleware: + (createMiddleware == null) ? [] : createMiddleware.handlers); - get('/:id', (req, res) async => - await this.read(req.params['id'], mergeMap([req.query, restProvider]))); + Middleware readMiddleware = _getAnnotation(this.read, Middleware); - patch('/:id', (req, res) async => await this.modify( - req.params['id'], req.body, restProvider)); + get( + '/:id', + (req, res) async => await this + .read(req.params['id'], mergeMap([req.query, restProvider])), + middleware: (readMiddleware == null) ? [] : readMiddleware.handlers); - post('/:id', (req, res) async => await this.update( - req.params['id'], req.body, restProvider)); + 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); - delete('/:id', (req, res) async => await this.remove( - req.params['id'], mergeMap([req.query, restProvider]))); + 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 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); } -} \ No newline at end of file +} diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index b3025980..ce744455 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -6,29 +6,32 @@ 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) : super() {} + HookedService(Service this.inner) { + // Clone all routes, including middleware + routes..clear()..addAll(inner.routes); + } @override Future index([Map params]) async { diff --git a/test/routing.dart b/test/routing.dart index e40fe288..e5c004b7 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -9,6 +9,14 @@ testMiddlewareMetadata(RequestContext req, ResponseContext res) async { return "This should not be shown."; } +class QueryService extends Service { + @override + @Middleware(const ['intercept_service', 'interceptor']) + read(id, [Map params]) { + return params; + } +} + main() { group('routing', () { Angel angel; @@ -22,10 +30,16 @@ main() { nested = new Angel(); todos = new Angel(); - angel.registerMiddleware('interceptor', (req, res) async { - res.write('Middleware'); - return false; - }); + 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)); @@ -37,12 +51,25 @@ main() { angel.post('/lambda', (req, res) => req.body); angel.use('/nes', nested); angel.use('/todos/:id', todos); - angel.get('/greet/:name', (RequestContext req, res) async => "Hello ${req.params['name']}").as('Named routes'); + angel + .get('/greet/:name', + (RequestContext req, res) async => "Hello ${req.params['name']}") + .as('Named routes'); angel.get('/named', (req, ResponseContext res) async { res.redirectTo('Named routes', {'name': 'tests'}); }); + angel.get('/log', (RequestContext req, res) async { + 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}"); + } + client = new http.Client(); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; @@ -97,8 +124,8 @@ main() { test('Can serialize function result as JSON', () async { Map headers = {'Content-Type': 'application/json'}; String postData = god.serialize({'it': 'works'}); - var response = await client.post( - "$url/lambda", headers: headers, body: postData); + var response = + await client.post("$url/lambda", headers: headers, body: postData); expect(god.deserialize(response.body)['it'], equals('works')); }); @@ -119,5 +146,16 @@ main() { print(response.body); expect(god.deserialize(response.body), equals('Hello tests')); }); + + 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"); + print(response.body); + expect(god.deserialize(response.body), equals('Logged')); + + response = await client.get("$url/query/foo?bar=baz"); + print(response.body); + expect(response.body, equals("Middleware")); + }); }); -} \ No newline at end of file +} From f49483ad030c96fa45f149707aeb69e8009a96fa Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 23 Jun 2016 15:09:49 -0400 Subject: [PATCH 025/414] Global service middleware + tests --- lib/src/http/service.dart | 6 ++++++ test/routing.dart | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 72340bdc..cdee9f21 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -60,6 +60,12 @@ class Service extends Routable { Service() : super() { Map restProvider = {'provider': Providers.REST}; + // Add global middleware if declared on the instance itself + Middleware before = _getAnnotation(this, Middleware); + if (before != null) { + routes.add(new Route("*", "*", before.handlers)); + } + Middleware indexMiddleware = _getAnnotation(this.index, Middleware); get('/', (req, res) async { return await this.index(mergeMap([req.query, restProvider])); diff --git a/test/routing.dart b/test/routing.dart index e5c004b7..cdcd3caf 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -9,9 +9,10 @@ testMiddlewareMetadata(RequestContext req, ResponseContext res) async { return "This should not be shown."; } +@Middleware(const ['intercept_service']) class QueryService extends Service { @override - @Middleware(const ['intercept_service', 'interceptor']) + @Middleware(const ['interceptor']) read(id, [Map params]) { return params; } From 12f8ffd7ae93253365ace70899ea788e1173f4ba Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 23 Jun 2016 15:10:49 -0400 Subject: [PATCH 026/414] 1.0.0-dev+1 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3a294e0b..ce31cfec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev +version: 1.0.0-dev+1 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From d3b486b283d492fb51ebad6ac8523ddb81003130 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 23 Jun 2016 16:11:40 -0400 Subject: [PATCH 027/414] Fixed hidden middleware bug --- lib/src/http/routable.dart | 3 +-- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 6e801dd0..721f04ce 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -70,8 +70,7 @@ class Routable extends Extensible { new RegExp(r'(^\/+)|(\/+$)'), '')] = service; _routable = service; } - - requestMiddleware.addAll(_routable.requestMiddleware); + for (Route route in _routable.routes) { Route provisional = new Route('', path); if (route.path == '/') { diff --git a/pubspec.yaml b/pubspec.yaml index ce31cfec..1bb11394 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev+1 +version: 1.0.0-dev+2 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 1899e586f1f0158aacd3ae707a6eaf96a680dd89 Mon Sep 17 00:00:00 2001 From: regiostech Date: Fri, 24 Jun 2016 15:19:02 -0400 Subject: [PATCH 028/414] Fixed more bugs with hooked services --- lib/src/http/server.dart | 2 +- lib/src/http/service_hooked.dart | 77 ++++++++++++++++++++++++++----- lib/src/http/services/memory.dart | 2 - test/hooked.dart | 2 + 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 2a44f126..0b4af3b8 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -195,7 +195,7 @@ class Angel extends Routable { if (routable is Service) { routable.app = this; } - super.use(path, routable, + return super.use(path, routable, hooked: hooked, middlewareNamespace: middlewareNamespace); } diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/service_hooked.dart index ce744455..cad0fc75 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/service_hooked.dart @@ -6,31 +6,84 @@ 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 all routes, including middleware - routes..clear()..addAll(inner.routes); + // Clone app instance + 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)); + } + + Middleware indexMiddleware = _getAnnotation(inner.index, Middleware); + get('/', (req, res) async { + return await this.index(mergeMap([req.query, restProvider])); + }, middleware: (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 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); + + 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); + + 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); + + 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); } @override diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 58425516..538c07f5 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -30,9 +30,7 @@ class MemoryService extends Service { Future create(data, [Map params]) async { //try { - print("Data: $data"); var created = (data is Map) ? god.deserializeDatum(data, outputType: T) : data; - print("Created $created"); items[items.length] = created; return _makeJson(items.length - 1, created); /*} catch (e) { diff --git a/test/hooked.dart b/test/hooked.dart index ac6afa52..f61512c5 100644 --- a/test/hooked.dart +++ b/test/hooked.dart @@ -6,6 +6,8 @@ import 'package:test/test.dart'; class Todo { String text; String over; + + Todo({String this.text, String this.over}); } main() { From 448c8dc99a8ee621dedb2e1f01d22094d6e5ef1e Mon Sep 17 00:00:00 2001 From: regiostech Date: Fri, 24 Jun 2016 15:41:41 -0400 Subject: [PATCH 029/414] Fixed some memory service bugs --- lib/src/http/services/memory.dart | 36 ++++++++++++++++++++----------- pubspec.yaml | 2 +- test/services.dart | 2 +- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 538c07f5..06a777f4 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -1,14 +1,23 @@ part of angel_framework.http; +/// Represents data that can be serialized into a MemoryService; +class MemoryModel { + int id; +} + /// An in-memory [Service]. class MemoryService extends Service { - Map items = {}; + Map items = {}; - _makeJson(int index, T t) { - if (T == null || T == Map) - return mergeMap([god.serializeObject(t), {'id': index}]); - else - return t; + MemoryService() :super() { + if (!reflectType(T).isAssignableTo(reflectType(MemoryModel))) { + throw new Exception( + "MemoryServices only support classes that inherit from MemoryModel."); + } + } + + _makeJson(int index, MemoryModel t) { + return t..id = index; } Future index([Map params]) async { @@ -21,7 +30,7 @@ class MemoryService extends Service { Future read(id, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { - T found = items[desiredId]; + MemoryModel found = items[desiredId]; if (found != null) { return _makeJson(desiredId, found); } else throw new AngelHttpException.NotFound(); @@ -30,9 +39,12 @@ class MemoryService extends Service { Future create(data, [Map params]) async { //try { - var created = (data is Map) ? god.deserializeDatum(data, outputType: T) : data; - items[items.length] = created; - return _makeJson(items.length - 1, created); + MemoryModel created = (data is MemoryModel) ? data : god.deserializeDatum( + data, outputType: T); + + created.id = items.length; + items[created.id] = created; + return created; /*} catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); }*/ @@ -69,11 +81,9 @@ class MemoryService extends Service { Future remove(id, [Map params]) async { int desiredId = int.parse(id.toString()); if (items.containsKey(desiredId)) { - T item = items[desiredId]; + MemoryModel item = items[desiredId]; items[desiredId] = null; return _makeJson(desiredId, item); } else throw new AngelHttpException.NotFound(); } - - MemoryService() : super(); } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 1bb11394..a0b77441 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev+2 +version: 1.0.0-dev+3 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/services.dart b/test/services.dart index 295e5812..d40d82e2 100644 --- a/test/services.dart +++ b/test/services.dart @@ -3,7 +3,7 @@ import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; import 'package:test/test.dart'; -class Todo { +class Todo extends MemoryModel { String text; String over; } From babb07c8149e565f2c54841271ff5afbf0377952 Mon Sep 17 00:00:00 2001 From: regiostech Date: Fri, 24 Jun 2016 16:38:29 -0400 Subject: [PATCH 030/414] Fixed some more memory service bugs --- lib/src/http/services/memory.dart | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 06a777f4..29a4adf1 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -7,7 +7,7 @@ class MemoryModel { /// An in-memory [Service]. class MemoryService extends Service { - Map items = {}; + List items = []; MemoryService() :super() { if (!reflectType(T).isAssignableTo(reflectType(MemoryModel))) { @@ -21,15 +21,18 @@ class MemoryService extends Service { } Future index([Map params]) async { - return items.keys - .where((index) => items[index] != null) - .map((index) => _makeJson(index, items[index])) - .toList(); + List result = []; + + for (int i = 0; i < items.length; i++) { + result.add(_makeJson(i, items[i])); + } + + return result; } Future read(id, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.containsKey(desiredId)) { + if (items.length > desiredId) { MemoryModel found = items[desiredId]; if (found != null) { return _makeJson(desiredId, found); @@ -43,7 +46,7 @@ class MemoryService extends Service { data, outputType: T); created.id = items.length; - items[created.id] = created; + items.add(created); return created; /*} catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); @@ -52,7 +55,7 @@ class MemoryService extends Service { Future modify(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.containsKey(desiredId)) { + if (items.length > desiredId) { try { Map existing = god.serializeObject(items[desiredId]); data = mergeMap([existing, data]); @@ -67,7 +70,7 @@ class MemoryService extends Service { Future update(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.containsKey(desiredId)) { + if (items.length > desiredId) { try { items[desiredId] = (data is Map) ? god.deserializeDatum(data, outputType: T) : data; @@ -80,9 +83,9 @@ class MemoryService extends Service { Future remove(id, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.containsKey(desiredId)) { + if (items.length > desiredId) { MemoryModel item = items[desiredId]; - items[desiredId] = null; + items.removeAt(desiredId); return _makeJson(desiredId, item); } else throw new AngelHttpException.NotFound(); } From 2459c82bf90e0ca5023a283d60d741ab82bec4bd Mon Sep 17 00:00:00 2001 From: regiostech Date: Fri, 24 Jun 2016 16:56:34 -0400 Subject: [PATCH 031/414] More memory service bugs --- lib/src/http/services/memory.dart | 25 +++++++++++-------------- pubspec.yaml | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 29a4adf1..06a777f4 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -7,7 +7,7 @@ class MemoryModel { /// An in-memory [Service]. class MemoryService extends Service { - List items = []; + Map items = {}; MemoryService() :super() { if (!reflectType(T).isAssignableTo(reflectType(MemoryModel))) { @@ -21,18 +21,15 @@ class MemoryService extends Service { } Future index([Map params]) async { - List result = []; - - for (int i = 0; i < items.length; i++) { - result.add(_makeJson(i, items[i])); - } - - return result; + return items.keys + .where((index) => items[index] != null) + .map((index) => _makeJson(index, items[index])) + .toList(); } Future read(id, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.length > desiredId) { + if (items.containsKey(desiredId)) { MemoryModel found = items[desiredId]; if (found != null) { return _makeJson(desiredId, found); @@ -46,7 +43,7 @@ class MemoryService extends Service { data, outputType: T); created.id = items.length; - items.add(created); + items[created.id] = created; return created; /*} catch (e) { throw new AngelHttpException.BadRequest(message: 'Invalid data.'); @@ -55,7 +52,7 @@ class MemoryService extends Service { Future modify(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.length > desiredId) { + if (items.containsKey(desiredId)) { try { Map existing = god.serializeObject(items[desiredId]); data = mergeMap([existing, data]); @@ -70,7 +67,7 @@ class MemoryService extends Service { Future update(id, data, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.length > desiredId) { + if (items.containsKey(desiredId)) { try { items[desiredId] = (data is Map) ? god.deserializeDatum(data, outputType: T) : data; @@ -83,9 +80,9 @@ class MemoryService extends Service { Future remove(id, [Map params]) async { int desiredId = int.parse(id.toString()); - if (items.length > desiredId) { + if (items.containsKey(desiredId)) { MemoryModel item = items[desiredId]; - items.removeAt(desiredId); + items[desiredId] = null; return _makeJson(desiredId, item); } else throw new AngelHttpException.NotFound(); } diff --git a/pubspec.yaml b/pubspec.yaml index a0b77441..d63a18b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev+3 +version: 1.0.0-dev+5 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From a7c8a95af7257e2962932b27fb950b2a464966d5 Mon Sep 17 00:00:00 2001 From: regiostech Date: Sun, 26 Jun 2016 20:20:42 -0400 Subject: [PATCH 032/414] Added Controllers, omg --- lib/defs.dart | 6 ++ lib/src/http/controller.dart | 99 +++++++++++++++++++++++++++++ lib/src/http/http.dart | 3 + lib/src/http/metadata/metadata.dart | 14 ++++ lib/src/http/response_context.dart | 21 ++++++ lib/src/http/routable.dart | 6 ++ lib/src/http/services/memory.dart | 5 -- pubspec.yaml | 2 +- test/common.dart | 8 +++ test/controller.dart | 79 +++++++++++++++++++++++ test/hooked.dart | 8 +-- 11 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 lib/defs.dart create mode 100644 lib/src/http/controller.dart create mode 100644 test/common.dart create mode 100644 test/controller.dart diff --git a/lib/defs.dart b/lib/defs.dart new file mode 100644 index 00000000..53d6074d --- /dev/null +++ b/lib/defs.dart @@ -0,0 +1,6 @@ +library angel_framework.defs; + +/// Represents data that can be serialized into a MemoryService; +class MemoryModel { + int id; +} \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart new file mode 100644 index 00000000..d19a98c9 --- /dev/null +++ b/lib/src/http/controller.dart @@ -0,0 +1,99 @@ +part of angel_framework.http; + +class Controller { + List middleware = []; + List routes = []; + Map _mappings = {}; + Expose exposeDecl; + + Future call(Angel app) async { + Routable routable = new Routable() + ..routes.addAll(routes); + app.use(exposeDecl.path, routable); + + TypeMirror typeMirror = reflectType(this.runtimeType); + String name = exposeDecl.as; + + if (name == null || name.isEmpty) + name = MirrorSystem.getName(typeMirror.simpleName); + + app.controllers[name] = this; + } + + 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 = []; + + try { + // 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 && + !exposeMirror.reflectee.allowNull.contain(name)) { + throw new AngelHttpException.BadRequest(); + } + + args.add(arg); + } + } + + return await instanceMirror + .invoke(key, args) + .reflectee; + } catch (e) { + throw new AngelHttpException(e); + } + }; + Route route = new Route( + exposeMirror.reflectee.method, + exposeMirror.reflectee.path, + [handler]..addAll(exposeMirror.reflectee.middleware)); + routes.add(route); + + String name = exposeMirror.reflectee.as; + + if (name == null || name.isEmpty) + name = MirrorSystem.getName(key); + + _mappings[name] = route; + } + } + }); + } +} \ No newline at end of file diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 44c163e1..da16ecd9 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -10,7 +10,9 @@ import 'package:body_parser/body_parser.dart'; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'package:mime/mime.dart'; +import '../../defs.dart'; +part 'controller.dart'; part 'extensible.dart'; part 'errors.dart'; part 'metadata/metadata.dart'; @@ -22,3 +24,4 @@ part 'server.dart'; part 'service.dart'; part 'service_hooked.dart'; part 'services/memory.dart'; + diff --git a/lib/src/http/metadata/metadata.dart b/lib/src/http/metadata/metadata.dart index b65fd84a..af164495 100644 --- a/lib/src/http/metadata/metadata.dart +++ b/lib/src/http/metadata/metadata.dart @@ -10,4 +10,18 @@ class Middleware { /// Annotation to set a service up to release hooks on every action. class Hooked { const Hooked(); +} + +class Expose { + final String method; + final Pattern path; + final List middleware; + final String as; + final List allowNull; + + const Expose(Pattern this.path, + {String this.method: "GET", + List this.middleware: const [], + String this.as: null, + List this.allowNull: const[]}); } \ No newline at end of file diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 0b46cfd1..42c1a0e4 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -103,6 +103,27 @@ class ResponseContext extends Extensible { throw new ArgumentError.notNull('Route to redirect to ($name)'); } + /// Redirects to the given [Controller] action. + redirectToAction(String action, [Map params, int code]) { + // UserController@show + List split = action.split("@"); + + if (split.length < 2) + throw new Exception("Controller redirects must take the form of 'Controller@action'. You gave: $action"); + + Controller controller = app.controller(split[0]); + + if (controller == null) + throw new Exception("Could not find a controller named '${split[0]}'"); + + Route matched = controller._mappings[split[1]]; + + if (matched == null) + throw new Exception("Controller '${split[0]}' does not contain any action named '${split[1]}'"); + + return redirect(matched.makeUri(params), code: code); + } + /// Streams a file to this response as chunked data. /// /// Useful for video sites. diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 721f04ce..e3dd0439 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -37,6 +37,9 @@ class Routable extends Extensible { /// A set of [Service] objects that have been mapped into routes. Map services = {}; + /// A set of [Controller] objects that have been loaded into the application. + Map controllers = {}; + /// Assigns a middleware to a name for convenience. registerMiddleware(String name, RequestMiddleware middleware) { this.requestMiddleware[name] = middleware; @@ -45,6 +48,9 @@ class Routable extends Extensible { /// Retrieves the service assigned to the given 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, diff --git a/lib/src/http/services/memory.dart b/lib/src/http/services/memory.dart index 06a777f4..7619f8c5 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/services/memory.dart @@ -1,10 +1,5 @@ part of angel_framework.http; -/// Represents data that can be serialized into a MemoryService; -class MemoryModel { - int id; -} - /// An in-memory [Service]. class MemoryService extends Service { Map items = {}; diff --git a/pubspec.yaml b/pubspec.yaml index d63a18b4..852d5697 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev+5 +version: 1.0.0-dev+6 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/common.dart b/test/common.dart new file mode 100644 index 00000000..54a28992 --- /dev/null +++ b/test/common.dart @@ -0,0 +1,8 @@ +library angel_framework.test.common; + +class Todo { + String text; + String over; + + Todo({String this.text, String this.over}); +} diff --git a/test/controller.dart b/test/controller.dart new file mode 100644 index 00000000..41faed23 --- /dev/null +++ b/test/controller.dart @@ -0,0 +1,79 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; +import 'common.dart'; + +@Expose("/todos", middleware: const ["foo"]) +class TodoController extends Controller { + List todos = [new Todo(text: "Hello", over: "world")]; + + @Expose("/:id", middleware: const["bar"]) + Future fetchTodo(int id, RequestContext req, + ResponseContext res) async { + expect(req, isNotNull); + expect(res, isNotNull); + return todos[id]; + } + + @Expose("/namedRoute/:foo", as: "foo") + Future someRandomRoute(RequestContext req, ResponseContext res) async { + return "${req.params['foo']}!"; + } +} + +main() { + group("controller", () { + Angel app = new Angel(); + HttpServer server; + InternetAddress host = InternetAddress.LOOPBACK_IP_V4; + int port = 3000; + http.Client client; + String url = "http://${host.address}:$port"; + + setUp(() async { + app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); + app.registerMiddleware("bar", (req, res) async => res.write("world!")); + app.get("/redirect", (req, ResponseContext res) async => + res.redirectToAction("TodoController@foo", {"foo": "world"})); + await app.configure(new TodoController()); + + print(app.controllers); + print("\nDUMPING ROUTES:"); + app.routes.forEach((Route route) { + print("\t${route.method} ${route.path} -> ${route.handlers}"); + }); + print("\n"); + + server = await app.startServer(host, port); + client = new http.Client(); + }); + + tearDown(() async { + await server.close(force: true); + client.close(); + client = null; + }); + + test("middleware", () async { + var response = await client.get("$url/todos/0"); + print(response.body); + + expect(response.body.indexOf("Hello, "), equals(0)); + + Map todo = JSON.decode(response.body.substring(7)); + expect(todo.keys.length, equals(2)); + expect(todo['text'], equals("Hello")); + expect(todo['over'], equals("world")); + }); + + test("named actions", () async { + var response = await client.get("$url/redirect"); + print(response.body); + + expect(response.body, equals("Hello, \"world!\"")); + }); + }); +} diff --git a/test/hooked.dart b/test/hooked.dart index f61512c5..c4d5616f 100644 --- a/test/hooked.dart +++ b/test/hooked.dart @@ -2,13 +2,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; import 'package:test/test.dart'; - -class Todo { - String text; - String over; - - Todo({String this.text, String this.over}); -} +import 'common.dart'; main() { group('Hooked', () { From 2a62bd8eb6b5878c247bf8a27c9441c9943f067b Mon Sep 17 00:00:00 2001 From: regiostech Date: Sun, 26 Jun 2016 21:27:40 -0400 Subject: [PATCH 033/414] Fixed makeUri??? --- lib/src/http/route.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 43c071f9..1abcb771 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -46,11 +46,11 @@ class Route { String result = path; if (params != null) { for (String key in (params.keys)) { - result = result.replaceAll(new RegExp(":$key"), params[key].toString()); + result = result.replaceAll(new RegExp(":$key" + r"\??"), params[key].toString()); } } - return result; + return result.replaceAll("*", ""); } /// Enables one or more handlers to be called whenever this route is visited. From 85b529ef00699fe2fadd0c3e40bfa0d904d0be78 Mon Sep 17 00:00:00 2001 From: regiostech Date: Fri, 1 Jul 2016 15:18:37 -0400 Subject: [PATCH 034/414] Working on changes, in line with angel_examples --- .gitignore | 1 + lib/src/http/controller.dart | 4 +++- lib/src/http/routable.dart | 10 +++++++++- pubspec.yaml | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 917abe7c..951d3784 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ packages # Include when developing application packages. pubspec.lock +doc/api \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index d19a98c9..6f795afc 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -83,7 +83,9 @@ class Controller { Route route = new Route( exposeMirror.reflectee.method, exposeMirror.reflectee.path, - [handler]..addAll(exposeMirror.reflectee.middleware)); + [] + ..addAll(exposeMirror.reflectee.middleware) + ..add(handler)); routes.add(route); String name = exposeMirror.reflectee.as; diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index e3dd0439..3722a4b8 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -76,7 +76,15 @@ class Routable extends Extensible { new RegExp(r'(^\/+)|(\/+$)'), '')] = service; _routable = service; } - + + if (_routable is Angel) { + 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 == '/') { diff --git a/pubspec.yaml b/pubspec.yaml index 852d5697..31832234 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev+6 +version: 1.0.0-dev.8 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 1a7d831a36548f509cc92c2c936e9e55758b18c5 Mon Sep 17 00:00:00 2001 From: regiostech Date: Sun, 3 Jul 2016 18:23:55 -0400 Subject: [PATCH 035/414] DOCTYPE -> !DOCTYPE --- lib/src/http/server.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 0b4af3b8..ac4ecbc7 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -19,7 +19,7 @@ class Angel extends Routable { AngelErrorHandler _errorHandler = (AngelHttpException e, req, ResponseContext res) { res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.status(e.statusCode); - res.write("${e.message}"); + res.write("${e.message}"); res.write("

    ${e.message}

      "); for (String error in e.errors) { res.write("
    • $error
    • "); From 76d26ea9c5f0abd1cb1764bf45b64e709da81ee6 Mon Sep 17 00:00:00 2001 From: regiostech Date: Mon, 4 Jul 2016 14:06:31 -0400 Subject: [PATCH 036/414] Controllers should be able to handle errors themselves --- lib/src/http/controller.dart | 42 +++++----- lib/src/http/response_context.dart | 2 +- lib/src/http/server.dart | 128 ++++++++++++++++------------- pubspec.yaml | 2 +- 4 files changed, 90 insertions(+), 84 deletions(-) diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 6f795afc..85d8b17d 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -52,33 +52,29 @@ class Controller { ResponseContext res) async { List args = []; - try { - // 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]; + // 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 && - !exposeMirror.reflectee.allowNull.contain(name)) { - throw new AngelHttpException.BadRequest(); - } - - args.add(arg); + if (arg == null && + !exposeMirror.reflectee.allowNull.contain(name)) { + throw new AngelHttpException.BadRequest(); } - } - return await instanceMirror - .invoke(key, args) - .reflectee; - } catch (e) { - throw new AngelHttpException(e); + args.add(arg); + } } + + return await instanceMirror + .invoke(key, args) + .reflectee; }; Route route = new Route( exposeMirror.reflectee.method, diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 42c1a0e4..cfef32a9 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -83,7 +83,7 @@ class ResponseContext extends Extensible {

      Currently redirecting you...


      - Click if you are not automatically redirected... + Click here if you are not automatically redirected... diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index ac4ecbc7..b49329f1 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -12,6 +12,12 @@ typedef Future AngelConfigurer(Angel app); /// A powerful real-time/REST/MVC server class. class Angel extends Routable { + var _beforeProcessed = new StreamController(); + var _afterProcessed = new StreamController(); + + Stream get beforeProcessed => _beforeProcessed.stream; + Stream get afterProcessed => _afterProcessed.stream; + ServerGenerator _serverGenerator = (address, port) async => await HttpServer.bind(address, port); @@ -50,69 +56,72 @@ class Angel extends Routable { await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); this.httpServer = server; - server.listen((HttpRequest request) async { - String req_url = - request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), ''); - if (req_url.isEmpty) req_url = '/'; - RequestContext req = await RequestContext.from(request, {}, this, null); - ResponseContext res = await ResponseContext.from(request.response, this); - - bool canContinue = true; - - var execHandler = (handler, req) async { - if (canContinue) { - canContinue = await new Future.sync(() async { - return _applyHandler(handler, req, res); - }).catchError((e, [StackTrace stackTrace]) async { - if (e is AngelHttpException) { - // Special handling for AngelHttpExceptions :) - try { - res.status(e.statusCode); - String accept = request.headers.value(HttpHeaders.ACCEPT); - if (accept == "*/*" || - accept.contains("application/json") || - 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 execHandler(handler, req); - } - - for (Route route in routes) { - if (!canContinue) break; - if (route.matcher.hasMatch(req_url) && - (request.method == route.method || route.method == '*')) { - req.params = route.parseParameters(req_url); - req.route = route; - - for (var handler in route.handlers) { - await execHandler(handler, req); - } - } - } - - for (var handler in after) { - await execHandler(handler, req); - } - _finalizeResponse(request, res); - }); + server.listen(handleRequest); return server; } + Future handleRequest(HttpRequest request) async { + _beforeProcessed.add(request); + String req_url = + request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), ''); + if (req_url.isEmpty) req_url = '/'; + RequestContext req = await RequestContext.from(request, {}, this, null); + ResponseContext res = await ResponseContext.from(request.response, this); + + bool canContinue = true; + + var execHandler = (handler, req) async { + if (canContinue) { + canContinue = await new Future.sync(() async { + return _applyHandler(handler, req, res); + }).catchError((e, [StackTrace stackTrace]) async { + if (e is AngelHttpException) { + // Special handling for AngelHttpExceptions :) + try { + res.status(e.statusCode); + String accept = request.headers.value(HttpHeaders.ACCEPT); + if (accept == "*/*" || + accept.contains("application/json") || + 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 execHandler(handler, req); + } + + for (Route route in routes) { + if (!canContinue) break; + if (route.matcher.hasMatch(req_url) && + (request.method == route.method || route.method == '*')) { + req.params = route.parseParameters(req_url); + req.route = route; + + for (var handler in route.handlers) { + await execHandler(handler, req); + } + } + } + + for (var handler in after) { + await execHandler(handler, req); + } + _finalizeResponse(request, res); + } + Future _applyHandler( handler, RequestContext req, ResponseContext res) async { if (handler is RequestMiddleware) { @@ -162,6 +171,7 @@ class Angel extends Routable { if (!res.willCloseItself) { res.responseData.forEach((blob) => request.response.add(blob)); await request.response.close(); + _afterProcessed.add(request); } } catch (e) { // Remember: This fails silently diff --git a/pubspec.yaml b/pubspec.yaml index 31832234..5fdf2a91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.8 +version: 1.0.0-dev.13 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 4c9db6f9327efbec74958cdd3aa5900a47433ccf Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 5 Jul 2016 18:11:54 -0400 Subject: [PATCH 037/414] onService, onController --- lib/src/http/controller.dart | 2 ++ lib/src/http/routable.dart | 12 +++++++++++- lib/src/http/server.dart | 33 ++++++++++++++++++++++++--------- pubspec.yaml | 2 +- test/services.dart | 1 + 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 85d8b17d..369b9b7b 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -1,12 +1,14 @@ part of angel_framework.http; class Controller { + Angel app; List middleware = []; List routes = []; Map _mappings = {}; Expose exposeDecl; Future call(Angel app) async { + this.app = app; Routable routable = new Routable() ..routes.addAll(routes); app.use(exposeDecl.path, routable); diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 3722a4b8..45a6637e 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -40,6 +40,13 @@ class Routable extends Extensible { /// A set of [Controller] objects that have been loaded into the application. Map controllers = {}; + StreamController _onService = new StreamController.broadcast(); + + /// Fired whenever a service is added to this instance. + /// + /// **NOTE**: This is a broadcast stream. + Stream get onService => _onService.stream; + /// Assigns a middleware to a name for convenience. registerMiddleware(String name, RequestMiddleware middleware) { this.requestMiddleware[name] = middleware; @@ -61,7 +68,7 @@ class Routable extends Extensible { /// For example, if the [Routable] has a middleware 'y', and the `middlewareNamespace` /// is 'x', then that middleware will be available as 'x.y' in the main application. /// These namespaces can be nested. - use(Pattern path, Routable routable, + void use(Pattern path, Routable routable, {bool hooked: true, String middlewareNamespace: null}) { Routable _routable = routable; @@ -115,6 +122,9 @@ class Routable extends Extensible { 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 diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index b49329f1..7bc9bce0 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -4,8 +4,8 @@ part of angel_framework.http; typedef Future ServerGenerator(InternetAddress address, int port); /// Handles an [AngelHttpException]. -typedef Future AngelErrorHandler( - AngelHttpException err, RequestContext req, ResponseContext res); +typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, + ResponseContext res); /// A function that configures an [Angel] server in some way. typedef Future AngelConfigurer(Angel app); @@ -14,15 +14,25 @@ typedef Future AngelConfigurer(Angel app); class Angel extends Routable { var _beforeProcessed = new StreamController(); var _afterProcessed = new StreamController(); + var _onController = new StreamController.broadcast(); + /// Fired before a request is processed. Always runs. Stream get beforeProcessed => _beforeProcessed.stream; + + /// Fired after a request is processed. Always runs. Stream get afterProcessed => _afterProcessed.stream; + /// Fired whenever a controller is added to this instance. + /// + /// **NOTE**: This is a broadcast stream. + Stream get onController => _onController.stream; + ServerGenerator _serverGenerator = (address, port) async => await HttpServer.bind(address, port); /// Default error handler, show HTML error page - AngelErrorHandler _errorHandler = (AngelHttpException e, req, ResponseContext res) { + AngelErrorHandler _errorHandler = (AngelHttpException e, req, + ResponseContext res) { res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.status(e.statusCode); res.write("${e.message}"); @@ -37,7 +47,8 @@ class Angel extends Routable { /// A function that renders views. /// /// Called by [ResponseContext]@`render`. - ViewGenerator viewGenerator = (String view, [Map data]) async => "No view engine has been configured yet."; + ViewGenerator viewGenerator = (String view, + [Map data]) async => "No view engine has been configured yet."; /// [RequestMiddleware] to be run before all requests. List before = []; @@ -53,7 +64,7 @@ class Angel extends Routable { /// Returns false on failure; otherwise, returns the HttpServer. startServer(InternetAddress address, int port) async { var server = - await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); + await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); this.httpServer = server; server.listen(handleRequest); @@ -64,7 +75,8 @@ class Angel extends Routable { Future handleRequest(HttpRequest request) async { _beforeProcessed.add(request); String req_url = - request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll(new RegExp(r'\/+$'), ''); + request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll( + new RegExp(r'\/+$'), ''); if (req_url.isEmpty) req_url = '/'; RequestContext req = await RequestContext.from(request, {}, this, null); ResponseContext res = await ResponseContext.from(request.response, this); @@ -122,8 +134,8 @@ class Angel extends Routable { _finalizeResponse(request, res); } - Future _applyHandler( - handler, RequestContext req, ResponseContext res) async { + Future _applyHandler(handler, RequestContext req, + ResponseContext res) async { if (handler is RequestMiddleware) { var result = await handler(req, res); if (result is bool) @@ -190,6 +202,9 @@ class Angel extends Routable { /// Applies an [AngelConfigurer] to this instance. Future configure(AngelConfigurer configurer) async { await configurer(this); + + if (configurer is Controller) + _onController.add(configurer); } /// Starts the server. @@ -231,7 +246,7 @@ class Angel extends Routable { : super() { _serverGenerator = (InternetAddress address, int port) async { var certificateChain = - Platform.script.resolve('server_chain.pem').toFilePath(); + Platform.script.resolve('server_chain.pem').toFilePath(); var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); diff --git a/pubspec.yaml b/pubspec.yaml index 5fdf2a91..0a670cd2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.13 +version: 1.0.0-dev.14 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/services.dart b/test/services.dart index d40d82e2..160ab668 100644 --- a/test/services.dart +++ b/test/services.dart @@ -1,3 +1,4 @@ +import 'package:angel_framework/defs.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; From 41b891039ec725d6a66bd05f98ab4b0057b38836 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 13 Sep 2016 23:01:22 -0400 Subject: [PATCH 038/414] Create TODO.md --- TODO.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..8d74b2ae --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +* Support for [Trestle](https://github.com/dart-bridge/trestle), use this as default, set up migration system around this +* Angel CLI +* Angel bootstrap project +* More docs +* Make tutorials, videos +* Launch! From 6d9fa4304c563d9c72ce844eba339b3f755e0dd8 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 13 Sep 2016 23:03:19 -0400 Subject: [PATCH 039/414] Update TODO.md --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 8d74b2ae..79881fd8 100644 --- a/TODO.md +++ b/TODO.md @@ -4,3 +4,5 @@ * More docs * Make tutorials, videos * Launch! +* Get rid of `part` and `part of`, so that we can make as much isomorphic as possible +* Get a nice launch process, so we can pre-compile things before running. Also support a sort of hot-reload From 4cab64f14f2ced3177cb06c337488893eefd189f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Sep 2016 15:53:01 -0400 Subject: [PATCH 040/414] Got rid of 'part', added 'export'. Also fixed attachment --- TODO.md | 1 - lib/angel_framework.dart | 3 +- lib/{ => src}/defs.dart | 0 lib/src/{http => }/extensible.dart | 4 +- lib/src/http/angel_base.dart | 15 ++ ...{errors.dart => angel_http_exception.dart} | 6 +- lib/src/http/controller.dart | 48 ++-- ...ervice_hooked.dart => hooked_service.dart} | 23 +- lib/src/http/http.dart | 37 +-- .../memory.dart => memory_service.dart} | 10 +- lib/src/http/{metadata => }/metadata.dart | 2 +- lib/src/http/request_context.dart | 21 +- lib/src/http/response_context.dart | 22 +- lib/src/http/routable.dart | 49 ++-- lib/src/http/route.dart | 2 +- lib/src/http/server.dart | 27 +- lib/src/http/service.dart | 28 +- lib/src/util.dart | 26 ++ test/all_tests.dart | 12 + test/common.dart | 4 +- test/controller.dart | 85 ++++--- test/hooked.dart | 4 +- test/routing.dart | 240 +++++++++--------- test/services.dart | 190 +++++++------- 24 files changed, 472 insertions(+), 387 deletions(-) rename lib/{ => src}/defs.dart (100%) rename lib/src/{http => }/extensible.dart (92%) create mode 100644 lib/src/http/angel_base.dart rename lib/src/http/{errors.dart => angel_http_exception.dart} (96%) rename lib/src/http/{service_hooked.dart => hooked_service.dart} (92%) rename lib/src/http/{services/memory.dart => memory_service.dart} (90%) rename lib/src/http/{metadata => }/metadata.dart (93%) create mode 100644 lib/src/util.dart create mode 100644 test/all_tests.dart diff --git a/TODO.md b/TODO.md index 79881fd8..3381eeda 100644 --- a/TODO.md +++ b/TODO.md @@ -4,5 +4,4 @@ * More docs * Make tutorials, videos * Launch! -* Get rid of `part` and `part of`, so that we can make as much isomorphic as possible * Get a nice launch process, so we can pre-compile things before running. Also support a sort of hot-reload diff --git a/lib/angel_framework.dart b/lib/angel_framework.dart index 7a624d51..636c6ca2 100644 --- a/lib/angel_framework.dart +++ b/lib/angel_framework.dart @@ -1,4 +1,5 @@ /// An easily-extensible web server framework in Dart. library angel_framework; -export 'src/http/http.dart'; \ No newline at end of file +export 'src/http/http.dart'; +export 'src/defs.dart'; \ No newline at end of file diff --git a/lib/defs.dart b/lib/src/defs.dart similarity index 100% rename from lib/defs.dart rename to lib/src/defs.dart diff --git a/lib/src/http/extensible.dart b/lib/src/extensible.dart similarity index 92% rename from lib/src/http/extensible.dart rename to lib/src/extensible.dart index 5ed88f44..b14b8255 100644 --- a/lib/src/http/extensible.dart +++ b/lib/src/extensible.dart @@ -1,4 +1,6 @@ -part of angel_framework.http; +library angel_framework.extensible; + +import 'dart:mirrors'; /// Supports accessing members of a Map as though they were actual members. class Extensible { diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart new file mode 100644 index 00000000..d95e5cf4 --- /dev/null +++ b/lib/src/http/angel_base.dart @@ -0,0 +1,15 @@ +library angel_framework.http.angel_base; + +import 'dart:async'; +import 'routable.dart'; + +/// A function that asynchronously generates a view from the given path and data. +typedef Future ViewGenerator(String path, [Map data]); + +class AngelBase extends Routable { + /// A function that renders views. + /// + /// Called by [ResponseContext]@`render`. + ViewGenerator viewGenerator = (String view, + [Map data]) async => "No view engine has been configured yet."; +} \ No newline at end of file diff --git a/lib/src/http/errors.dart b/lib/src/http/angel_http_exception.dart similarity index 96% rename from lib/src/http/errors.dart rename to lib/src/http/angel_http_exception.dart index 84fd482f..3e91b312 100644 --- a/lib/src/http/errors.dart +++ b/lib/src/http/angel_http_exception.dart @@ -1,4 +1,4 @@ -part of angel_framework.http; +library angel_framework.http.angel_http_exception; class _AngelHttpExceptionBase implements Exception { /// An HTTP status code this exception will throw. @@ -18,7 +18,7 @@ class _AngelHttpExceptionBase implements Exception { return "$statusCode: $message"; } - Map toMap() { + Map toJson() { return { 'isError': true, 'statusCode': statusCode, @@ -26,6 +26,8 @@ class _AngelHttpExceptionBase implements Exception { 'errors': errors }; } + + Map toMap() => toJson(); } /// Basically the same as diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 369b9b7b..99d6c0d7 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -1,27 +1,22 @@ -part of angel_framework.http; +library angel_framework.http.controller; + +import 'dart:async'; +import 'dart:mirrors'; +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 { - Angel app; + AngelBase app; List middleware = []; List routes = []; - Map _mappings = {}; + Map routeMappings = {}; Expose exposeDecl; - Future call(Angel app) async { - this.app = app; - Routable routable = new Routable() - ..routes.addAll(routes); - app.use(exposeDecl.path, routable); - - TypeMirror typeMirror = reflectType(this.runtimeType); - String name = exposeDecl.as; - - if (name == null || name.isEmpty) - name = MirrorSystem.getName(typeMirror.simpleName); - - app.controllers[name] = this; - } - Controller() { // Load global expose decl ClassMirror classMirror = reflectClass(this.runtimeType); @@ -91,9 +86,24 @@ class Controller { if (name == null || name.isEmpty) name = MirrorSystem.getName(key); - _mappings[name] = route; + routeMappings[name] = route; } } }); } + + Future call(AngelBase app) async { + this.app = app; + app.use(exposeDecl.path, generateRoutable()); + + TypeMirror typeMirror = reflectType(this.runtimeType); + String name = exposeDecl.as; + + if (name == null || name.isEmpty) + name = MirrorSystem.getName(typeMirror.simpleName); + + app.controllers[name] = this; + } + + Routable generateRoutable() => new Routable()..routes.addAll(routes); } \ No newline at end of file diff --git a/lib/src/http/service_hooked.dart b/lib/src/http/hooked_service.dart similarity index 92% rename from lib/src/http/service_hooked.dart rename to lib/src/http/hooked_service.dart index cad0fc75..3fe18df4 100644 --- a/lib/src/http/service_hooked.dart +++ b/lib/src/http/hooked_service.dart @@ -1,4 +1,11 @@ -part of angel_framework.http; +library angel_framework.http; + +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. class HookedService extends Service { @@ -38,22 +45,22 @@ class HookedService extends Service { Map restProvider = {'provider': Providers.REST}; // Add global middleware if declared on the instance itself - Middleware before = _getAnnotation(inner, Middleware); + Middleware before = getAnnotation(inner, Middleware); if (before != null) { routes.add(new Route("*", "*", before.handlers)); } - Middleware indexMiddleware = _getAnnotation(inner.index, Middleware); + Middleware indexMiddleware = getAnnotation(inner.index, Middleware); get('/', (req, res) async { return await this.index(mergeMap([req.query, restProvider])); }, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers); - Middleware createMiddleware = _getAnnotation(inner.create, Middleware); + Middleware createMiddleware = getAnnotation(inner.create, Middleware); post('/', (req, res) async => await this.create(req.body, restProvider), middleware: (createMiddleware == null) ? [] : createMiddleware.handlers); - Middleware readMiddleware = _getAnnotation(inner.read, Middleware); + Middleware readMiddleware = getAnnotation(inner.read, Middleware); get( '/:id', @@ -61,7 +68,7 @@ class HookedService extends Service { .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: (readMiddleware == null) ? [] : readMiddleware.handlers); - Middleware modifyMiddleware = _getAnnotation(inner.modify, Middleware); + Middleware modifyMiddleware = getAnnotation(inner.modify, Middleware); patch( '/:id', (req, res) async => @@ -69,7 +76,7 @@ class HookedService extends Service { middleware: (modifyMiddleware == null) ? [] : modifyMiddleware.handlers); - Middleware updateMiddleware = _getAnnotation(inner.update, Middleware); + Middleware updateMiddleware = getAnnotation(inner.update, Middleware); post( '/:id', (req, res) async => @@ -77,7 +84,7 @@ class HookedService extends Service { middleware: (updateMiddleware == null) ? [] : updateMiddleware.handlers); - Middleware removeMiddleware = _getAnnotation(inner.remove, Middleware); + Middleware removeMiddleware = getAnnotation(inner.remove, Middleware); delete( '/:id', (req, res) async => await this diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index da16ecd9..80028e27 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -1,27 +1,16 @@ -/// HTTP logic +/// Various libraries useful for creating highly-extensible servers. library angel_framework.http; -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'dart:mirrors'; -import 'package:body_parser/body_parser.dart'; -import 'package:json_god/json_god.dart' as god; -import 'package:merge_map/merge_map.dart'; -import 'package:mime/mime.dart'; -import '../../defs.dart'; - -part 'controller.dart'; -part 'extensible.dart'; -part 'errors.dart'; -part 'metadata/metadata.dart'; -part 'request_context.dart'; -part 'response_context.dart'; -part 'route.dart'; -part 'routable.dart'; -part 'server.dart'; -part 'service.dart'; -part 'service_hooked.dart'; -part 'services/memory.dart'; +export 'angel_base.dart'; +export 'angel_http_exception.dart'; +export 'controller.dart'; +export 'hooked_service.dart'; +export 'metadata.dart'; +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/services/memory.dart b/lib/src/http/memory_service.dart similarity index 90% rename from lib/src/http/services/memory.dart rename to lib/src/http/memory_service.dart index 7619f8c5..7e68599e 100644 --- a/lib/src/http/services/memory.dart +++ b/lib/src/http/memory_service.dart @@ -1,4 +1,12 @@ -part of angel_framework.http; +library angel_framework.http.memory_service; + +import 'dart:async'; +import 'dart:mirrors'; +import 'package:json_god/json_god.dart' as god; +import 'package:merge_map/merge_map.dart'; +import '../defs.dart'; +import 'angel_http_exception.dart'; +import 'service.dart'; /// An in-memory [Service]. class MemoryService extends Service { diff --git a/lib/src/http/metadata/metadata.dart b/lib/src/http/metadata.dart similarity index 93% rename from lib/src/http/metadata/metadata.dart rename to lib/src/http/metadata.dart index af164495..1e96c449 100644 --- a/lib/src/http/metadata/metadata.dart +++ b/lib/src/http/metadata.dart @@ -1,4 +1,4 @@ -part of angel_framework.http; +library angel_framework.http.metadata; /// Annotation to map middleware onto a handler. class Middleware { diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index da2beca5..3ae4d88a 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -1,18 +1,15 @@ -part of angel_framework.http; - -/// A function that intercepts a request and determines whether handling of it should continue. -typedef Future RequestMiddleware(RequestContext req, ResponseContext res); - -/// A function that receives an incoming [RequestContext] and responds to it. -typedef Future RequestHandler(RequestContext req, ResponseContext res); - -/// A function that handles an [HttpRequest]. -typedef Future RawRequestHandler(HttpRequest request); +library angel_framework.http.request_context; +import 'dart:async'; +import 'dart:io'; +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 { /// The [Angel] instance that is responding to this request. - Angel app; + AngelBase app; /// Any cookies sent with this request. List get cookies => underlyingRequest.cookies; @@ -66,7 +63,7 @@ class RequestContext extends Extensible { /// Magically transforms an [HttpRequest] into a RequestContext. static Future from(HttpRequest request, - Map parameters, Angel app, Route sourceRoute) async { + Map parameters, AngelBase app, Route sourceRoute) async { RequestContext context = new RequestContext(); context.app = app; diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index cfef32a9..5067b88c 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -1,12 +1,19 @@ -part of angel_framework.http; +library angel_framework.http.response_context; -/// A function that asynchronously generates a view from the given path and data. -typedef Future ViewGenerator(String path, [Map data]); +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +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 { /// The [Angel] instance that is sending a response. - Angel app; + AngelBase app; /// Can we still write to this response? bool isOpen = true; @@ -32,8 +39,7 @@ class ResponseContext extends Extensible { /// Sends a download as a response. download(File file, {String filename}) { - header("Content-Disposition", - 'Content-Disposition: attachment; filename="${filename ?? file.path}"'); + 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()); @@ -116,7 +122,7 @@ class ResponseContext extends Extensible { if (controller == null) throw new Exception("Could not find a controller named '${split[0]}'"); - Route matched = controller._mappings[split[1]]; + Route matched = controller.routeMappings[split[1]]; if (matched == null) throw new Exception("Controller '${split[0]}' does not contain any action named '${split[1]}'"); @@ -147,7 +153,7 @@ class ResponseContext extends Extensible { /// Magically transforms an [HttpResponse] object into a ResponseContext. static Future from - (HttpResponse response, Angel app) async + (HttpResponse response, AngelBase app) async { ResponseContext context = new ResponseContext(response); context.app = app; diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 45a6637e..4d65ce72 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -1,30 +1,29 @@ -part of angel_framework.http; +library angel_framework.http.routable; + +import 'dart:async'; +import 'dart:io'; +import 'dart:mirrors'; +import '../extensible.dart'; +import '../util.dart'; +import 'angel_base.dart'; +import 'controller.dart'; +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}); -_matchingAnnotation(List metadata, Type T) { - for (InstanceMirror metaDatum in metadata) { - if (metaDatum.hasReflectee) { - var reflectee = metaDatum.reflectee; - if (reflectee.runtimeType == T) { - return reflectee; - } - } - } - return null; -} +/// A function that intercepts a request and determines whether handling of it should continue. +typedef Future RequestMiddleware(RequestContext req, ResponseContext res); -_getAnnotation(obj, Type T) { - if (obj is Function || obj is Future) { - MethodMirror methodMirror = (reflect(obj) as ClosureMirror).function; - return _matchingAnnotation(methodMirror.metadata, T); - } else { - ClassMirror classMirror = reflectClass(obj.runtimeType); - return _matchingAnnotation(classMirror.metadata, T); - } +/// A function that receives an incoming [RequestContext] and responds to it. +typedef Future RequestHandler(RequestContext req, ResponseContext res); - return null; -} +/// A function that handles an [HttpRequest]. +typedef Future RawRequestHandler(HttpRequest request); /// A routable server that can handle dynamic requests. class Routable extends Extensible { @@ -75,7 +74,7 @@ class Routable extends Extensible { // 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); + Hooked hookedDeclaration = getAnnotation(_routable, Hooked); Service service = (hookedDeclaration != null || hooked) ? new HookedService(_routable) : _routable; @@ -84,7 +83,7 @@ class Routable extends Extensible { _routable = service; } - if (_routable is Angel) { + if (_routable is AngelBase) { all(path, (RequestContext req, ResponseContext res) async { req.app = _routable; res.app = _routable; @@ -135,7 +134,7 @@ class Routable extends Extensible { List handlers = []; // Merge @Middleware declaration, if any - Middleware middlewareDeclaration = _getAnnotation( + Middleware middlewareDeclaration = getAnnotation( handler, Middleware); if (middlewareDeclaration != null) { handlers.addAll(middlewareDeclaration.handlers); diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 1abcb771..ab3588de 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -1,4 +1,4 @@ -part of angel_framework.http; +library angel_framework.http.route; /// Represents an endpoint open for connection via the Internet. class Route { diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 7bc9bce0..001dee81 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -1,4 +1,17 @@ -part of angel_framework.http; +library angel_framework.http.server; + +import 'dart:async'; +import 'dart:io'; +import 'dart:math' show Random; +import 'package:json_god/json_god.dart' as god; +import 'angel_base.dart'; +import 'angel_http_exception.dart'; +import 'controller.dart'; +import 'request_context.dart'; +import 'response_context.dart'; +import 'routable.dart'; +import 'route.dart'; +import 'service.dart'; /// A function that binds an [Angel] server to an Internet address and port. typedef Future ServerGenerator(InternetAddress address, int port); @@ -7,11 +20,11 @@ typedef Future ServerGenerator(InternetAddress address, int port); typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, ResponseContext res); -/// A function that configures an [Angel] server in some way. -typedef Future AngelConfigurer(Angel app); +/// A function that configures an [AngelBase] server in some way. +typedef Future AngelConfigurer(AngelBase app); /// A powerful real-time/REST/MVC server class. -class Angel extends Routable { +class Angel extends AngelBase { var _beforeProcessed = new StreamController(); var _afterProcessed = new StreamController(); var _onController = new StreamController.broadcast(); @@ -44,12 +57,6 @@ class Angel extends Routable { res.end(); }; - /// A function that renders views. - /// - /// Called by [ResponseContext]@`render`. - ViewGenerator viewGenerator = (String view, - [Map data]) async => "No view engine has been configured yet."; - /// [RequestMiddleware] to be run before all requests. List before = []; diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index cdee9f21..c12e8a35 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -1,4 +1,14 @@ -part of angel_framework.http; +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. /// @@ -25,7 +35,7 @@ class Providers { /// Heavily inspired by FeathersJS. <3 class Service extends Routable { /// The [Angel] app powering this service. - Angel app; + AngelBase app; /// Retrieves all resources. Future index([Map params]) { @@ -61,22 +71,22 @@ class Service extends Routable { Map restProvider = {'provider': Providers.REST}; // Add global middleware if declared on the instance itself - Middleware before = _getAnnotation(this, Middleware); + Middleware before = getAnnotation(this, Middleware); if (before != null) { routes.add(new Route("*", "*", before.handlers)); } - Middleware indexMiddleware = _getAnnotation(this.index, Middleware); + Middleware indexMiddleware = getAnnotation(this.index, Middleware); get('/', (req, res) async { return await this.index(mergeMap([req.query, restProvider])); }, middleware: (indexMiddleware == null) ? [] : indexMiddleware.handlers); - Middleware createMiddleware = _getAnnotation(this.create, Middleware); + Middleware createMiddleware = getAnnotation(this.create, Middleware); post('/', (req, res) async => await this.create(req.body, restProvider), middleware: (createMiddleware == null) ? [] : createMiddleware.handlers); - Middleware readMiddleware = _getAnnotation(this.read, Middleware); + Middleware readMiddleware = getAnnotation(this.read, Middleware); get( '/:id', @@ -84,7 +94,7 @@ class Service extends Routable { .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: (readMiddleware == null) ? [] : readMiddleware.handlers); - Middleware modifyMiddleware = _getAnnotation(this.modify, Middleware); + Middleware modifyMiddleware = getAnnotation(this.modify, Middleware); patch( '/:id', (req, res) async => @@ -92,7 +102,7 @@ class Service extends Routable { middleware: (modifyMiddleware == null) ? [] : modifyMiddleware.handlers); - Middleware updateMiddleware = _getAnnotation(this.update, Middleware); + Middleware updateMiddleware = getAnnotation(this.update, Middleware); post( '/:id', (req, res) async => @@ -100,7 +110,7 @@ class Service extends Routable { middleware: (updateMiddleware == null) ? [] : updateMiddleware.handlers); - Middleware removeMiddleware = _getAnnotation(this.remove, Middleware); + Middleware removeMiddleware = getAnnotation(this.remove, Middleware); delete( '/:id', (req, res) async => await this diff --git a/lib/src/util.dart b/lib/src/util.dart new file mode 100644 index 00000000..ad614889 --- /dev/null +++ b/lib/src/util.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'dart:mirrors'; + +matchingAnnotation(List metadata, Type T) { + for (InstanceMirror metaDatum in metadata) { + if (metaDatum.hasReflectee) { + var reflectee = metaDatum.reflectee; + if (reflectee.runtimeType == T) { + return reflectee; + } + } + } + return null; +} + +getAnnotation(obj, Type T) { + if (obj is Function || obj is Future) { + MethodMirror methodMirror = (reflect(obj) as ClosureMirror).function; + return matchingAnnotation(methodMirror.metadata, T); + } else { + ClassMirror classMirror = reflectClass(obj.runtimeType); + return matchingAnnotation(classMirror.metadata, T); + } + + return null; +} \ No newline at end of file diff --git a/test/all_tests.dart b/test/all_tests.dart new file mode 100644 index 00000000..1df0368c --- /dev/null +++ b/test/all_tests.dart @@ -0,0 +1,12 @@ +import 'package:test/test.dart'; +import 'controller.dart' as controller; +import 'hooked.dart' as hooked; +import 'routing.dart' as routing; +import 'services.dart' as services; + +main() { + group('controller', controller.main); + group('hooked', hooked.main); + group('routing', routing.main); + group('services', services.main); +} \ No newline at end of file diff --git a/test/common.dart b/test/common.dart index 54a28992..cb9ffe1e 100644 --- a/test/common.dart +++ b/test/common.dart @@ -1,6 +1,8 @@ library angel_framework.test.common; -class Todo { +import 'package:angel_framework/src/defs.dart'; + +class Todo extends MemoryModel { String text; String over; diff --git a/test/controller.dart b/test/controller.dart index 41faed23..c85a9037 100644 --- a/test/controller.dart +++ b/test/controller.dart @@ -19,61 +19,62 @@ class TodoController extends Controller { } @Expose("/namedRoute/:foo", as: "foo") - Future someRandomRoute(RequestContext req, ResponseContext res) async { + Future someRandomRoute(RequestContext req, + ResponseContext res) async { return "${req.params['foo']}!"; } } main() { - group("controller", () { - Angel app = new Angel(); - HttpServer server; - InternetAddress host = InternetAddress.LOOPBACK_IP_V4; - int port = 3000; - http.Client client; - String url = "http://${host.address}:$port"; + Angel app = new Angel(); + HttpServer server; + InternetAddress host = InternetAddress.LOOPBACK_IP_V4; + int port = 3000; + http.Client client; + String url = "http://${host.address}:$port"; - setUp(() async { - app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); - app.registerMiddleware("bar", (req, res) async => res.write("world!")); - app.get("/redirect", (req, ResponseContext res) async => - res.redirectToAction("TodoController@foo", {"foo": "world"})); - await app.configure(new TodoController()); + setUp(() async { + app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); + app.registerMiddleware("bar", (req, res) async => res.write("world!")); + app.get("/redirect", (req, ResponseContext res) async => + res.redirectToAction("TodoController@foo", {"foo": "world"})); + await app.configure(new TodoController()); - print(app.controllers); - print("\nDUMPING ROUTES:"); - app.routes.forEach((Route route) { - print("\t${route.method} ${route.path} -> ${route.handlers}"); - }); - print("\n"); - - server = await app.startServer(host, port); - client = new http.Client(); + print(app.controllers); + print("\nDUMPING ROUTES:"); + app.routes.forEach((Route route) { + print("\t${route.method} ${route.path} -> ${route.handlers}"); }); + print("\n"); - tearDown(() async { - await server.close(force: true); - client.close(); - client = null; - }); + server = await app.startServer(host, port); + client = new http.Client(); + }); - test("middleware", () async { - var response = await client.get("$url/todos/0"); - print(response.body); + tearDown(() async { + await server.close(force: true); + client.close(); + client = null; + }); - expect(response.body.indexOf("Hello, "), equals(0)); + test("middleware", () async { + var rgx = new RegExp("^Hello, world!"); + var response = await client.get("$url/todos/0"); + print(response.body); - Map todo = JSON.decode(response.body.substring(7)); - expect(todo.keys.length, equals(2)); - expect(todo['text'], equals("Hello")); - expect(todo['over'], equals("world")); - }); + expect(response.body.indexOf("Hello, "), equals(0)); - test("named actions", () async { - var response = await client.get("$url/redirect"); - print(response.body); + Map todo = JSON.decode(response.body.replaceAll(rgx, "")); + print("Todo: $todo"); + expect(todo.keys.length, equals(3)); + expect(todo['text'], equals("Hello")); + expect(todo['over'], equals("world")); + }); - expect(response.body, equals("Hello, \"world!\"")); - }); + test("named actions", () async { + var response = await client.get("$url/redirect"); + print(response.body); + + expect(response.body, equals("Hello, \"world!\"")); }); } diff --git a/test/hooked.dart b/test/hooked.dart index c4d5616f..b72d7930 100644 --- a/test/hooked.dart +++ b/test/hooked.dart @@ -5,8 +5,7 @@ import 'package:test/test.dart'; import 'common.dart'; main() { - group('Hooked', () { - Map headers = { + Map headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' }; @@ -77,5 +76,4 @@ main() { List result = god.deserialize(response.body); expect(result[0]["angel"], equals("framework")); }); - }); } \ No newline at end of file diff --git a/test/routing.dart b/test/routing.dart index cdcd3caf..d72b1a90 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -19,144 +19,140 @@ class QueryService extends Service { } main() { - group('routing', () { - Angel angel; - Angel nested; - Angel todos; - String url; - http.Client client; + Angel angel; + Angel nested; + Angel todos; + String url; + http.Client client; - setUp(() async { - angel = new Angel(); - nested = new Angel(); - todos = new Angel(); + setUp(() async { + angel = new Angel(); + nested = new Angel(); + todos = new Angel(); - 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)); - 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']}") - .as('Named routes'); - angel.get('/named', (req, ResponseContext res) async { - res.redirectTo('Named routes', {'name': 'tests'}); - }); - angel.get('/log', (RequestContext req, res) async { - 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}"); - } - - client = new http.Client(); - await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); - url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + angel..registerMiddleware('interceptor', (req, res) async { + res.write('Middleware'); + return false; + })..registerMiddleware('intercept_service', + (RequestContext req, res) async { + print("Intercepting a service!"); + return true; }); - tearDown(() async { - await angel.httpServer.close(force: true); - angel = null; - nested = null; - todos = null; - client.close(); - client = null; - url = null; + todos.get('/action/:action', (req, res) => res.json(req.params)); + nested.post('/ted/:route', (req, res) => res.json(req.params)); + angel.get('/meta', testMiddlewareMetadata); + angel.get('/intercepted', 'This should not be shown', + middleware: ['interceptor']); + angel.get('/hello', 'world'); + 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']}") + .as('Named routes'); + angel.get('/named', (req, ResponseContext res) async { + res.redirectTo('Named routes', {'name': 'tests'}); }); - - test('Can match basic url', () async { - var response = await client.get("$url/hello"); - expect(response.body, equals('"world"')); + angel.get('/log', (RequestContext req, res) async { + print("Query: ${req.query}"); + return "Logged"; }); + angel.use('/query', new QueryService()); + angel.get('*', 'MJ'); - test('Can match url with multiple parameters', () async { - var response = await client.get('$url/name/HELLO/last/WORLD'); - var json = god.deserialize(response.body); - expect(json['first'], equals('HELLO')); - expect(json['last'], equals('WORLD')); - }); + print("DUMPING ROUTES: "); + for (Route route in angel.routes) { + print("${route.method} ${route.path} - ${route.handlers}"); + } - test('Can nest another Angel instance', () async { - var response = await client.post('$url/nes/ted/foo'); - var json = god.deserialize(response.body); - expect(json['route'], equals('foo')); - }); + client = new http.Client(); + await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + }); - 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); - expect(json['id'], equals(1337)); - expect(json['action'], equals('test')); - }); + tearDown(() async { + await angel.httpServer.close(force: true); + angel = null; + nested = null; + todos = null; + client.close(); + client = null; + url = null; + }); - test('Can add and use named middleware', () async { - var response = await client.get('$url/intercepted'); - expect(response.body, equals('Middleware')); - }); + test('Can match basic url', () async { + var response = await client.get("$url/hello"); + expect(response.body, equals('"world"')); + }); - test('Middleware via metadata', () async { - // Metadata - var response = await client.get('$url/meta'); - expect(response.body, equals('Middleware')); - }); + test('Can match url with multiple parameters', () async { + var response = await client.get('$url/name/HELLO/last/WORLD'); + var json = god.deserialize(response.body); + expect(json['first'], equals('HELLO')); + expect(json['last'], equals('WORLD')); + }); - test('Can serialize function result as JSON', () async { - Map headers = {'Content-Type': 'application/json'}; - String postData = god.serialize({'it': 'works'}); - var response = - await client.post("$url/lambda", headers: headers, body: postData); - expect(god.deserialize(response.body)['it'], equals('works')); - }); + test('Can nest another Angel instance', () async { + var response = await client.post('$url/nes/ted/foo'); + var json = god.deserialize(response.body); + expect(json['route'], equals('foo')); + }); - test('Fallback routes', () async { - var response = await client.get('$url/my_favorite_artist'); - expect(response.body, equals('"MJ"')); - }); + 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); + expect(json['id'], equals(1337)); + expect(json['action'], equals('test')); + }); - test('Can name routes', () { - Route foo = angel.get('/framework/:id', 'Angel').as('frm'); - String uri = foo.makeUri({'id': 'angel'}); - print(uri); - expect(uri, equals('/framework/angel')); - }); + test('Can add and use named middleware', () async { + var response = await client.get('$url/intercepted'); + expect(response.body, equals('Middleware')); + }); - test('Redirect to named routes', () async { - var response = await client.get('$url/named'); - print(response.body); - expect(god.deserialize(response.body), equals('Hello tests')); - }); + test('Middleware via metadata', () async { + // Metadata + var response = await client.get('$url/meta'); + expect(response.body, equals('Middleware')); + }); - 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"); - print(response.body); - expect(god.deserialize(response.body), equals('Logged')); + test('Can serialize function result as JSON', () async { + Map headers = {'Content-Type': 'application/json'}; + String postData = god.serialize({'it': 'works'}); + var response = + await client.post("$url/lambda", headers: headers, body: postData); + expect(god.deserialize(response.body)['it'], equals('works')); + }); - response = await client.get("$url/query/foo?bar=baz"); - print(response.body); - expect(response.body, equals("Middleware")); - }); + test('Fallback routes', () async { + var response = await client.get('$url/my_favorite_artist'); + expect(response.body, equals('"MJ"')); + }); + + test('Can name routes', () { + Route foo = angel.get('/framework/:id', 'Angel').as('frm'); + String uri = foo.makeUri({'id': 'angel'}); + print(uri); + expect(uri, equals('/framework/angel')); + }); + + test('Redirect to named routes', () async { + var response = await client.get('$url/named'); + print(response.body); + expect(god.deserialize(response.body), equals('Hello tests')); + }); + + 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"); + print(response.body); + expect(god.deserialize(response.body), equals('Logged')); + + response = await client.get("$url/query/foo?bar=baz"); + print(response.body); + expect(response.body, equals("Middleware")); }); } diff --git a/test/services.dart b/test/services.dart index 160ab668..70cf063e 100644 --- a/test/services.dart +++ b/test/services.dart @@ -1,4 +1,4 @@ -import 'package:angel_framework/defs.dart'; +import 'package:angel_framework/src/defs.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; @@ -10,109 +10,107 @@ class Todo extends MemoryModel { } main() { - group('Services', () { - Map headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }; - Angel app; - String url; - http.Client client; + Map headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + Angel app; + String url; + http.Client client; - setUp(() async { - app = new Angel(); - client = new http.Client(); - Service todos = new MemoryService(); - app.use('/todos', todos); - print(app.service("todos")); - await app.startServer(null, 0); - url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + setUp(() async { + app = new Angel(); + client = new http.Client(); + Service todos = new MemoryService(); + app.use('/todos', todos); + print(app.service("todos")); + await app.startServer(null, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); + + tearDown(() async { + app = null; + url = null; + client.close(); + client = null; + }); + + group('memory', () { + test('can index an empty service', () async { + var response = await client.get("$url/todos/"); + print(response.body); + expect(response.body, equals('[]')); + for (int i = 0; i < 3; i++) { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + } + response = await client.get("$url/todos"); + print(response.body); + expect(god + .deserialize(response.body) + .length, equals(3)); }); - tearDown(() async { - app = null; - url = null; - client.close(); - client = null; + test('can create data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + var response = await client.post( + "$url/todos", headers: headers, body: postData); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('Hello, world!')); }); - group('memory', () { - test('can index an empty service', () async { - var response = await client.get("$url/todos/"); - print(response.body); - expect(response.body, equals('[]')); - for (int i = 0; i < 3; i++) { - String postData = god.serialize({'text': 'Hello, world!'}); - await client.post( - "$url/todos", headers: headers, body: postData); - } - response = await client.get("$url/todos"); - print(response.body); - expect(god - .deserialize(response.body) - .length, equals(3)); - }); + test('can fetch data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + var response = await client.get( + "$url/todos/0"); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('Hello, world!')); + }); - test('can create data', () async { - String postData = god.serialize({'text': 'Hello, world!'}); - var response = await client.post( - "$url/todos", headers: headers, body: postData); - var json = god.deserialize(response.body); - print(json); - expect(json['text'], equals('Hello, world!')); - }); + test('can modify data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + postData = god.serialize({'text': 'modified'}); + var response = await client.patch( + "$url/todos/0", headers: headers, body: postData); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('modified')); + }); - test('can fetch data', () async { - String postData = god.serialize({'text': 'Hello, world!'}); - await client.post( - "$url/todos", headers: headers, body: postData); - var response = await client.get( - "$url/todos/0"); - var json = god.deserialize(response.body); - print(json); - expect(json['text'], equals('Hello, world!')); - }); + test('can overwrite data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + postData = god.serialize({'over': 'write'}); + var response = await client.post( + "$url/todos/0", headers: headers, body: postData); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals(null)); + expect(json['over'], equals('write')); + }); - test('can modify data', () async { - String postData = god.serialize({'text': 'Hello, world!'}); - await client.post( - "$url/todos", headers: headers, body: postData); - postData = god.serialize({'text': 'modified'}); - var response = await client.patch( - "$url/todos/0", headers: headers, body: postData); - var json = god.deserialize(response.body); - print(json); - expect(json['text'], equals('modified')); - }); - - test('can overwrite data', () async { - String postData = god.serialize({'text': 'Hello, world!'}); - await client.post( - "$url/todos", headers: headers, body: postData); - postData = god.serialize({'over': 'write'}); - var response = await client.post( - "$url/todos/0", headers: headers, body: postData); - var json = god.deserialize(response.body); - print(json); - expect(json['text'], equals(null)); - expect(json['over'], equals('write')); - }); - - test('can delete data', () async { - String postData = god.serialize({'text': 'Hello, world!'}); - await client.post( - "$url/todos", headers: headers, body: postData); - var response = await client.delete( - "$url/todos/0"); - var json = god.deserialize(response.body); - print(json); - expect(json['text'], equals('Hello, world!')); - response = await client.get("$url/todos"); - print(response.body); - expect(god - .deserialize(response.body) - .length, equals(0)); - }); + test('can delete data', () async { + String postData = god.serialize({'text': 'Hello, world!'}); + await client.post( + "$url/todos", headers: headers, body: postData); + var response = await client.delete( + "$url/todos/0"); + var json = god.deserialize(response.body); + print(json); + expect(json['text'], equals('Hello, world!')); + response = await client.get("$url/todos"); + print(response.body); + expect(god + .deserialize(response.body) + .length, equals(0)); }); }); } \ No newline at end of file From 52c5112d9369df1b0cca5c59a8f670ea5146d20e Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Sep 2016 15:53:25 -0400 Subject: [PATCH 041/414] 1.0.0-dev.15 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0a670cd2..b8f78b59 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.14 +version: 1.0.0-dev.15 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From dfbfc4cbcfef2537b5355b2340ba007904d69464 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 12:12:25 -0400 Subject: [PATCH 042/414] Dependency Injection! :) --- lib/src/http/angel_base.dart | 5 + lib/src/http/base_middleware.dart | 9 ++ lib/src/http/base_plugin.dart | 8 ++ lib/src/http/controller.dart | 24 +++-- lib/src/http/server.dart | 163 +++++++++++++++++++++--------- pubspec.yaml | 7 +- test/all_tests.dart | 2 + test/di.dart | 87 ++++++++++++++++ 8 files changed, 249 insertions(+), 56 deletions(-) create mode 100644 lib/src/http/base_middleware.dart create mode 100644 lib/src/http/base_plugin.dart create mode 100644 test/di.dart diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart index d95e5cf4..2229c02d 100644 --- a/lib/src/http/angel_base.dart +++ b/lib/src/http/angel_base.dart @@ -1,12 +1,17 @@ library angel_framework.http.angel_base; import 'dart:async'; +import 'package:container/container.dart'; import 'routable.dart'; /// A function that asynchronously generates a view from the given path and data. typedef Future ViewGenerator(String path, [Map data]); class AngelBase extends Routable { + Container _container = new Container(); + /// A [Container] used to inject dependencies. + Container get container => _container; + /// A function that renders views. /// /// Called by [ResponseContext]@`render`. diff --git a/lib/src/http/base_middleware.dart b/lib/src/http/base_middleware.dart new file mode 100644 index 00000000..ca3c5282 --- /dev/null +++ b/lib/src/http/base_middleware.dart @@ -0,0 +1,9 @@ +library angel_framework.http.base_middleware; + +import 'dart:async'; +import 'request_context.dart'; +import 'response_context.dart'; + +abstract class BaseMiddleware { + Future call(RequestContext req, ResponseContext res); +} \ No newline at end of file diff --git a/lib/src/http/base_plugin.dart b/lib/src/http/base_plugin.dart new file mode 100644 index 00000000..ec5ed301 --- /dev/null +++ b/lib/src/http/base_plugin.dart @@ -0,0 +1,8 @@ +library angel_framework.http.base_plugin; + +import 'dart:async'; +import 'server.dart'; + +abstract class AngelPlugin { + Future call(Angel app); +} \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 99d6c0d7..08f94aa1 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -56,16 +56,26 @@ class Controller { args.add(req); else if (parameter.type.reflectedType == ResponseContext) args.add(res); - else { - String name = MirrorSystem.getName(parameter.simpleName); + else {String name = MirrorSystem.getName(parameter.simpleName); var arg = req.params[name]; - if (arg == null && - !exposeMirror.reflectee.allowNull.contain(name)) { - throw new AngelHttpException.BadRequest(); - } + 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) { + // + } + } - args.add(arg); + if (!exposeMirror.reflectee.allowNull.contain(name)) + throw new AngelHttpException.BadRequest(message: "Missing parameter '$name'"); + + } else args.add(arg); } } diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 001dee81..6bffaa2a 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -3,6 +3,7 @@ library angel_framework.http.server; import 'dart:async'; import 'dart:io'; import 'dart:math' show Random; +import 'dart:mirrors'; import 'package:json_god/json_god.dart' as god; import 'angel_base.dart'; import 'angel_http_exception.dart'; @@ -12,40 +13,41 @@ import 'response_context.dart'; import 'routable.dart'; import 'route.dart'; import 'service.dart'; +export 'package:container/container.dart'; /// A function that binds an [Angel] server to an Internet address and port. typedef Future ServerGenerator(InternetAddress address, int port); /// Handles an [AngelHttpException]. -typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req, - ResponseContext res); +typedef Future AngelErrorHandler( + AngelHttpException err, RequestContext req, ResponseContext res); /// A function that configures an [AngelBase] server in some way. typedef Future AngelConfigurer(AngelBase app); /// A powerful real-time/REST/MVC server class. class Angel extends AngelBase { - var _beforeProcessed = new StreamController(); - var _afterProcessed = new StreamController(); + var _afterProcessed = new StreamController.broadcast(); + var _beforeProcessed = new StreamController.broadcast(); var _onController = new StreamController.broadcast(); + ServerGenerator _serverGenerator = + (address, port) async => await HttpServer.bind(address, port); + + /// Fired after a request is processed. Always runs. + Stream get afterProcessed => _afterProcessed.stream; /// Fired before a request is processed. Always runs. Stream get beforeProcessed => _beforeProcessed.stream; - /// Fired after a request is processed. Always runs. - Stream get afterProcessed => _afterProcessed.stream; /// Fired whenever a controller is added to this instance. /// /// **NOTE**: This is a broadcast stream. Stream get onController => _onController.stream; - ServerGenerator _serverGenerator = - (address, port) async => await HttpServer.bind(address, port); - /// Default error handler, show HTML error page - AngelErrorHandler _errorHandler = (AngelHttpException e, req, - ResponseContext res) { + AngelErrorHandler _errorHandler = + (AngelHttpException e, req, ResponseContext res) { res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString()); res.status(e.statusCode); res.write("${e.message}"); @@ -69,9 +71,9 @@ class Angel extends AngelBase { /// Starts the server. /// /// Returns false on failure; otherwise, returns the HttpServer. - startServer(InternetAddress address, int port) async { + Future startServer([InternetAddress address, int port]) async { var server = - await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port); + await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0); this.httpServer = server; server.listen(handleRequest); @@ -79,29 +81,42 @@ class Angel extends AngelBase { return server; } + /// Loads some base dependencies into the service container. + void bootstrapContainer() { + container.singleton(this, as: AngelBase); + container.singleton(this); + + if (runtimeType != Angel) + container.singleton(this, as: Angel); + } + Future handleRequest(HttpRequest request) async { _beforeProcessed.add(request); - String req_url = - request.uri.toString().replaceAll("?" + request.uri.query, "").replaceAll( - new RegExp(r'\/+$'), ''); - if (req_url.isEmpty) req_url = '/'; + + 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; - var execHandler = (handler, req) async { + executeHandler(handler, req) async { if (canContinue) { - canContinue = await new Future.sync(() async { - return _applyHandler(handler, req, res); - }).catchError((e, [StackTrace stackTrace]) async { + 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("application/json") || + accept.contains(ContentType.JSON.mimeType) || accept.contains("application/javascript")) { res.json(e.toMap()); } else { @@ -113,38 +128,41 @@ class Angel extends AngelBase { _onError(e, stackTrace); canContinue = false; return false; - }); + } } else return false; - }; + } for (var handler in before) { - await execHandler(handler, req); + await executeHandler(handler, req); } for (Route route in routes) { if (!canContinue) break; - if (route.matcher.hasMatch(req_url) && + + if (route.matcher.hasMatch(requestedUrl) && (request.method == route.method || route.method == '*')) { - req.params = route.parseParameters(req_url); + req.params = route.parseParameters(requestedUrl); req.route = route; for (var handler in route.handlers) { - await execHandler(handler, req); + await executeHandler(handler, req); } } } for (var handler in after) { - await execHandler(handler, req); + await executeHandler(handler, req); } + _finalizeResponse(request, res); } - Future _applyHandler(handler, RequestContext req, - ResponseContext res) async { + Future _applyHandler( + handler, RequestContext req, ResponseContext res) async { if (handler is RequestMiddleware) { var result = await handler(req, res); + if (result is bool) return result == true; else if (result != null) { @@ -157,7 +175,9 @@ class Angel extends AngelBase { if (handler is RequestHandler) { await handler(req, res); return res.isOpen; - } else if (handler is RawRequestHandler) { + } + + if (handler is RawRequestHandler) { var result = await handler(req.underlyingRequest); if (result is bool) return result == true; @@ -166,8 +186,10 @@ class Angel extends AngelBase { return false; } else return true; - } else if (handler is Function || handler is Future) { - var result = await handler(); + } + + if (handler is Future) { + var result = await handler; if (result is bool) return result == true; else if (result != null) { @@ -175,14 +197,27 @@ class Angel extends AngelBase { return false; } else return true; - } else if (requestMiddleware.containsKey(handler)) { - return await _applyHandler(requestMiddleware[handler], req, res); - } else { - res.willCloseItself = true; - res.underlyingResponse.write(god.serialize(handler)); - await res.underlyingResponse.close(); - return false; } + + if (handler is Function) { + var result = await runContained(handler, req, res); + if (result is bool) + return result == true; + else if (result != null) { + res.json(result); + return false; + } else + return true; + } + + if (requestMiddleware.containsKey(handler)) { + return await _applyHandler(requestMiddleware[handler], req, res); + } + + res.willCloseItself = true; + res.underlyingResponse.write(god.serialize(handler)); + await res.underlyingResponse.close(); + return false; } _finalizeResponse(HttpRequest request, ResponseContext res) async { @@ -193,7 +228,7 @@ class Angel extends AngelBase { _afterProcessed.add(request); } } catch (e) { - // Remember: This fails silently + failSilently(request, res); } } @@ -206,14 +241,47 @@ class Angel extends AngelBase { return new String.fromCharCodes(codeUnits); } + // Run a function after injecting from service container + Future runContained(Function handler, RequestContext req, ResponseContext res) async { + ClosureMirror closureMirror = reflect(handler); + List args = []; + + for (ParameterMirror parameter in closureMirror.function.parameters) { + if (parameter.type.reflectedType == RequestContext) + args.add(req); + else if (parameter.type.reflectedType == ResponseContext) + args.add(res); + else { + // First, search to see if we can map this to a type + if (parameter.type.reflectedType != dynamic) { + args.add(container.make(parameter.type.reflectedType)); + } else { + String name = MirrorSystem.getName(parameter.simpleName); + + if (name == "req") + args.add(req); + else if (name == "res") + args.add(res); + else { + throw new Exception("Cannot resolve parameter '$name' within handler."); + } + } + } + } + + return await closureMirror.apply(args).reflectee; + } + /// Applies an [AngelConfigurer] to this instance. Future configure(AngelConfigurer configurer) async { await configurer(this); - if (configurer is Controller) - _onController.add(configurer); + if (configurer is Controller) _onController.add(configurer); } + /// Fallback when an error is thrown while handling a request. + void failSilently(HttpRequest request, ResponseContext res) {} + /// Starts the server. void listen({InternetAddress address, int port: 3000}) { runZoned(() async { @@ -242,7 +310,9 @@ class Angel extends AngelBase { if (stackTrace != null) stderr.write(stackTrace.toString()); } - Angel() : super() {} + Angel() : super() { + bootstrapContainer(); + } /// Creates an HTTPS server. /// Provide paths to a certificate chain and server key (both .pem). @@ -251,9 +321,10 @@ class Angel extends AngelBase { Angel.secure(String certificateChainPath, String serverKeyPath, {String password}) : super() { + bootstrapContainer(); _serverGenerator = (InternetAddress address, int port) async { var certificateChain = - Platform.script.resolve('server_chain.pem').toFilePath(); + Platform.script.resolve('server_chain.pem').toFilePath(); var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); diff --git a/pubspec.yaml b/pubspec.yaml index b8f78b59..de100a9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,14 @@ name: angel_framework -version: 1.0.0-dev.15 +version: 1.0.0-dev.16 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework dependencies: 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" merge_map: ">=1.0.0 <2.0.0" mime: ">=0.9.3 <1.0.0" dev_dependencies: - http: ">= 0.11.3 < 0.12.0" - test: ">= 0.12.13 < 0.13.0" \ No newline at end of file + http: ">= 0.11.3 <0.12.0" + test: ">= 0.12.13 <0.13.0" \ No newline at end of file diff --git a/test/all_tests.dart b/test/all_tests.dart index 1df0368c..3b289eef 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -1,5 +1,6 @@ import 'package:test/test.dart'; import 'controller.dart' as controller; +import 'di.dart' as di; import 'hooked.dart' as hooked; import 'routing.dart' as routing; import 'services.dart' as services; @@ -7,6 +8,7 @@ import 'services.dart' as services; main() { group('controller', controller.main); group('hooked', hooked.main); + group('di', di.main); group('routing', routing.main); group('services', services.main); } \ No newline at end of file diff --git a/test/di.dart b/test/di.dart new file mode 100644 index 00000000..8e2bdc22 --- /dev/null +++ b/test/di.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; +import 'common.dart'; + +final String TEXT = "make your bed"; +final String OVER = "never"; + +main() { + Angel app; + http.Client client; + HttpServer server; + String url; + + setUp(() async { + app = new Angel(); + client = new http.Client(); + + // Inject some todos + app.container.singleton(new Todo(text: TEXT, over: OVER)); + + app.get("/errands", (Todo singleton) => singleton); + app.get("/errands3", (Errand singleton, Todo foo, RequestContext req) => singleton.text); + await app.configure(new SingletonController()); + await app.configure(new ErrandController()); + + server = await app.startServer(); + url = "http://${server.address.host}:${server.port}"; + }); + + tearDown(() async { + app = null; + url = null; + client.close(); + client = null; + await server.close(force: true); + }); + + test("singleton in route", () async { + validateTodoSingleton(await client.get("$url/errands")); + }); + + test("singleton in controller", () async { + validateTodoSingleton(await client.get("$url/errands2")); + }); + + test("make in route", () async { + var response = await client.get("$url/errands3"); + String text = await JSON.decode(response.body); + expect(text, equals(TEXT)); + }); + + test("make in controller", () async { + var response = await client.get("$url/errands4"); + String text = await JSON.decode(response.body); + expect(text, equals(TEXT)); + }); +} + +void validateTodoSingleton(response) { + Map todo = JSON.decode(response.body); + expect(todo.keys.length, equals(3)); + expect(todo["id"], equals(null)); + expect(todo["text"], equals(TEXT)); + expect(todo["over"], equals(OVER)); +} + +@Expose("/errands2") +class SingletonController extends Controller { + @Expose("/") + todo(Todo singleton) => singleton; +} + +@Expose("/errands4") +class ErrandController extends Controller { + @Expose("/") + errand(Errand errand) => errand.text; +} + +class Errand { + Todo todo; + String get text => todo.text; + + Errand(this.todo); +} \ No newline at end of file From 7077cf90891a0d34cbf6ca9919441263229fb43d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 12:13:52 -0400 Subject: [PATCH 043/414] Base --- lib/src/http/http.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart index 80028e27..92eb2c4c 100644 --- a/lib/src/http/http.dart +++ b/lib/src/http/http.dart @@ -3,6 +3,8 @@ library angel_framework.http; export 'angel_base.dart'; export 'angel_http_exception.dart'; +export 'base_middleware.dart'; +export 'base_plugin.dart'; export 'controller.dart'; export 'hooked_service.dart'; export 'metadata.dart'; From 35a0fedb2ef0084bfc36d84922e044918973f616 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 16:01:25 -0400 Subject: [PATCH 044/414] Inject from req.params too --- lib/src/http/server.dart | 2 ++ pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 6bffaa2a..528392aa 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -262,6 +262,8 @@ class Angel extends AngelBase { args.add(req); else if (name == "res") args.add(res); + else if (req.params.containsKey(name)) + args.add(req.params[name]); else { throw new Exception("Cannot resolve parameter '$name' within handler."); } diff --git a/pubspec.yaml b/pubspec.yaml index de100a9e..90a765db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.16 +version: 1.0.0-dev.17 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From bdcd9e251377c4809554cfa6a627d439c702fbba Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 21:44:00 -0400 Subject: [PATCH 045/414] Improved DI --- lib/src/http/server.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 528392aa..7efa66a5 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -242,7 +242,7 @@ class Angel extends AngelBase { } // Run a function after injecting from service container - Future runContained(Function handler, RequestContext req, ResponseContext res) async { + Future runContained(Function handler, RequestContext req, ResponseContext res, {Map namedParameters, Map injecting}) async { ClosureMirror closureMirror = reflect(handler); List args = []; @@ -254,7 +254,7 @@ class Angel extends AngelBase { else { // First, search to see if we can map this to a type if (parameter.type.reflectedType != dynamic) { - args.add(container.make(parameter.type.reflectedType)); + args.add(container.make(parameter.type.reflectedType, namedParameters: namedParameters, injecting: injecting)); } else { String name = MirrorSystem.getName(parameter.simpleName); diff --git a/pubspec.yaml b/pubspec.yaml index 90a765db..66f86f61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.17 +version: 1.0.0-dev.18 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From aea4ec1cf2629a0f9aa9a1f13fb30191d1962fed Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 22:59:06 -0400 Subject: [PATCH 046/414] Improved DI 2 --- lib/src/http/server.dart | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 7efa66a5..7400a136 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -39,7 +39,6 @@ class Angel extends AngelBase { /// Fired before a request is processed. Always runs. Stream get beforeProcessed => _beforeProcessed.stream; - /// Fired whenever a controller is added to this instance. /// /// **NOTE**: This is a broadcast stream. @@ -72,8 +71,8 @@ class Angel extends AngelBase { /// /// Returns false on failure; otherwise, returns the HttpServer. Future startServer([InternetAddress address, int port]) async { - var server = - await _serverGenerator(address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0); + var server = await _serverGenerator( + address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0); this.httpServer = server; server.listen(handleRequest); @@ -86,8 +85,7 @@ class Angel extends AngelBase { container.singleton(this, as: AngelBase); container.singleton(this); - if (runtimeType != Angel) - container.singleton(this, as: Angel); + if (runtimeType != Angel) container.singleton(this, as: Angel); } Future handleRequest(HttpRequest request) async { @@ -242,7 +240,9 @@ class Angel extends AngelBase { } // Run a function after injecting from service container - Future runContained(Function handler, RequestContext req, ResponseContext res, {Map namedParameters, Map injecting}) async { + Future runContained(Function handler, RequestContext req, ResponseContext res, + {Map namedParameters, + Map injecting}) async { ClosureMirror closureMirror = reflect(handler); List args = []; @@ -254,18 +254,20 @@ class Angel extends AngelBase { else { // First, search to see if we can map this to a type if (parameter.type.reflectedType != dynamic) { - args.add(container.make(parameter.type.reflectedType, namedParameters: namedParameters, injecting: injecting)); + args.add(container.make(parameter.type.reflectedType, + namedParameters: namedParameters, injecting: injecting)); } else { String name = MirrorSystem.getName(parameter.simpleName); - if (name == "req") + if (req.params.containsKey(name)) + args.add(req.params[name]); + else if (name == "req") args.add(req); else if (name == "res") args.add(res); - else if (req.params.containsKey(name)) - args.add(req.params[name]); else { - throw new Exception("Cannot resolve parameter '$name' within handler."); + throw new Exception( + "Cannot resolve parameter '$name' within handler."); } } } From 2cb63347a758dc65d8af8a3de4b190eb3c2e547f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Mon, 19 Sep 2016 02:52:21 -0400 Subject: [PATCH 047/414] Better errors kinda --- lib/src/http/server.dart | 8 ++++++++ pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 7400a136..3525ef09 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -29,6 +29,7 @@ typedef Future AngelConfigurer(AngelBase app); class Angel extends AngelBase { var _afterProcessed = new StreamController.broadcast(); var _beforeProcessed = new StreamController.broadcast(); + var _fatalErrorStream = new StreamController.broadcast(); var _onController = new StreamController.broadcast(); ServerGenerator _serverGenerator = (address, port) async => await HttpServer.bind(address, port); @@ -39,6 +40,9 @@ class Angel extends AngelBase { /// Fired before a request is processed. Always runs. Stream get beforeProcessed => _beforeProcessed.stream; + /// Fired on fatal errors. + Stream get fatalErrorStream => _fatalErrorStream.stream; + /// Fired whenever a controller is added to this instance. /// /// **NOTE**: This is a broadcast stream. @@ -312,6 +316,10 @@ class Angel extends AngelBase { _onError(e, [StackTrace stackTrace]) { stderr.write(e.toString()); if (stackTrace != null) stderr.write(stackTrace.toString()); + _fatalErrorStream.add({ + "error": e, + "stack": stackTrace + }); } Angel() : super() { diff --git a/pubspec.yaml b/pubspec.yaml index 66f86f61..46dc7166 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.18 +version: 1.0.0-dev.19 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 848cf542708acfc2112191b767657f61ef3d905d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 21 Sep 2016 01:10:21 -0400 Subject: [PATCH 048/414] get errorHandler --- lib/src/http/server.dart | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 3525ef09..cdc75279 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -62,6 +62,9 @@ class Angel extends AngelBase { res.end(); }; + /// The handler currently configured to run on [AngelHttpException]s. + AngelErrorHandler get errorHandler => _errorHandler; + /// [RequestMiddleware] to be run before all requests. List before = []; diff --git a/pubspec.yaml b/pubspec.yaml index 46dc7166..dd8a5f7d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.19 +version: 1.0.0-dev.20 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 376e2095bacfcddfe4560218cacd695c3d573d1b Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 21 Sep 2016 02:37:01 -0400 Subject: [PATCH 049/414] No more stderr --- lib/src/http/server.dart | 2 -- pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index cdc75279..a5757675 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -317,8 +317,6 @@ class Angel extends AngelBase { /// Handles a server error. _onError(e, [StackTrace stackTrace]) { - stderr.write(e.toString()); - if (stackTrace != null) stderr.write(stackTrace.toString()); _fatalErrorStream.add({ "error": e, "stack": stackTrace diff --git a/pubspec.yaml b/pubspec.yaml index dd8a5f7d..4e70a091 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.20 +version: 1.0.0-dev.21 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 1bb077a3d9b9c400d04add48f19e0d6b58e19987 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 7 Oct 2016 17:39:41 -0400 Subject: [PATCH 050/414] CORS-able --- lib/src/http/server.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index a5757675..197e982c 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -227,10 +227,10 @@ class Angel extends AngelBase { _finalizeResponse(HttpRequest request, ResponseContext res) async { try { + _afterProcessed.add(request); if (!res.willCloseItself) { res.responseData.forEach((blob) => request.response.add(blob)); await request.response.close(); - _afterProcessed.add(request); } } catch (e) { failSilently(request, res); diff --git a/pubspec.yaml b/pubspec.yaml index 4e70a091..abb87f81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.21 +version: 1.0.0-dev.22 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 551a7f086fb76da53a9fc5175fbe037445af206f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 22 Oct 2016 16:41:36 -0400 Subject: [PATCH 051/414] 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')); - */ }); }); } From eb24b0c43e76e04d69afdd590b5d394b8be45d4a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Mon, 21 Nov 2016 23:58:53 -0500 Subject: [PATCH 052/414] Will finish routing changes tmw, then resolve issues --- lib/src/http/server.dart | 10 ++++------ pubspec.yaml | 16 +++++++++------- test/controller.dart | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 5a47d8a9..420fa01a 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -97,10 +97,9 @@ class Angel extends AngelBase { /// /// Returns false on failure; otherwise, returns the HttpServer. Future startServer([InternetAddress address, int port]) async { - var server = await _serverGenerator( - address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0); - this.httpServer = server; - return server..listen(handleRequest); + final host = address ?? InternetAddress.LOOPBACK_IP_V4; + final server = await _serverGenerator(host, port ?? 0); + return this.httpServer = server..listen(handleRequest); } /// Loads some base dependencies into the service container. @@ -185,8 +184,7 @@ class Angel extends AngelBase { if (requestedUrl.isEmpty) requestedUrl = '/'; - final route = resolve(requestedUrl, - (route) => route.method == request.method || route.method == '*'); + final route = resolve(requestedUrl, method: request.method); print('Resolve ${requestedUrl} -> $route'); req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); diff --git a/pubspec.yaml b/pubspec.yaml index b2431cab..0d1e5031 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,14 +3,16 @@ version: 1.0.0-dev.22 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework +environment: + sdk: ">=1.18.0" 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" - merge_map: ">=1.0.0 <2.0.0" - mime: ">=0.9.3 <1.0.0" + body_parser: ^1.0.0-dev + container: ^0.1.2 + json_god: ^2.0.0-beta + merge_map: ^1.0.0 + mime: ^0.9.3 dev_dependencies: - http: ">= 0.11.3 <0.12.0" - test: ">= 0.12.13 <0.13.0" \ No newline at end of file + http: ^0.11.3 + test: ^0.12.13 diff --git a/test/controller.dart b/test/controller.dart index c85a9037..8af37c83 100644 --- a/test/controller.dart +++ b/test/controller.dart @@ -52,7 +52,7 @@ main() { }); tearDown(() async { - await server.close(force: true); + await (server ?? app.httpServer).close(force: true); client.close(); client = null; }); From 3355b0ab39f6981d44157f0dbdb4d77670893fe2 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 04:10:47 -0500 Subject: [PATCH 053/414] Finally switched to new router --- .travis.yml | 1 + README.md | 3 +- lib/src/http/controller.dart | 47 ++++++----- lib/src/http/hooked_service.dart | 13 +-- lib/src/http/response_context.dart | 18 ++++- lib/src/http/routable.dart | 23 +++--- lib/src/http/server.dart | 13 ++- lib/src/http/service.dart | 14 ++-- pubspec.yaml | 5 +- test/controller.dart | 30 +++---- test/di.dart | 3 +- test/hooked.dart | 123 ++++++++++++++++------------- test/routing.dart | 14 ++-- 13 files changed, 175 insertions(+), 132 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/README.md b/README.md index 0e96b466..bb83bbe0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # angel_framework -![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev-red.svg) +![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev.23-red.svg) +![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index f48516c5..7eb716ba 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -18,19 +18,6 @@ class Controller { Future call(AngelBase app) async { this.app = app; - app.use(exposeDecl.path, generateRoutable()); - - TypeMirror typeMirror = reflectType(this.runtimeType); - String name = exposeDecl.as; - - if (name == null || name.isEmpty) - name = MirrorSystem.getName(typeMirror.simpleName); - - app.controllers[name] = this; - } - - Routable generateRoutable() { - final routable = new Routable(); // Load global expose decl ClassMirror classMirror = reflectClass(this.runtimeType); @@ -47,11 +34,18 @@ class Controller { "All controllers must carry an @Expose() declaration."); } - final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware); + app.use(exposeDecl.path, generateRoutable(classMirror)); + TypeMirror typeMirror = reflectType(this.runtimeType); + String name = exposeDecl.as; - InstanceMirror instanceMirror = reflect(this); - classMirror.instanceMembers - .forEach((Symbol key, MethodMirror methodMirror) { + if (name == null || name.isEmpty) + name = MirrorSystem.getName(typeMirror.simpleName); + + app.controllers[name] = this; + } + + _callback(InstanceMirror instanceMirror, Routable routable, List handlers) { + return (Symbol key, MethodMirror methodMirror) { if (methodMirror.isRegularMethod && key != #toString && key != #noSuchMethod && @@ -102,11 +96,13 @@ class Controller { return await instanceMirror.invoke(key, args).reflectee; }; + final middleware = [] + ..addAll(handlers) + ..addAll(exposeMirror.reflectee.middleware); + final route = routable.addRoute(exposeMirror.reflectee.method, exposeMirror.reflectee.path, handler, - middleware: [] - ..addAll(handlers) - ..addAll(exposeMirror.reflectee.middleware)); + middleware: middleware); String name = exposeMirror.reflectee.as; @@ -115,7 +111,16 @@ class Controller { routeMappings[name] = route; } } - }); + }; + } + + Routable generateRoutable(ClassMirror classMirror) { + final routable = new Routable(debug: true); + final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware); + + InstanceMirror instanceMirror = reflect(this); + final callback = _callback(instanceMirror, routable, handlers); + classMirror.instanceMembers.forEach(callback); return routable; } diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart index 546e2e52..3f7552f8 100644 --- a/lib/src/http/hooked_service.dart +++ b/lib/src/http/hooked_service.dart @@ -37,7 +37,10 @@ class HookedService extends Service { HookedService(Service this.inner) { // Clone app instance if (inner.app != null) this.app = inner.app; + } + @override + void addRoutes() { // Set up our routes. We still need to copy middleware from inner service Map restProvider = {'provider': Providers.REST}; @@ -45,7 +48,7 @@ class HookedService extends Service { Middleware before = getAnnotation(inner, Middleware); final handlers = []; - if (before != null) handlers.add(before.handlers); + if (before != null) handlers.addAll(before.handlers); Middleware indexMiddleware = getAnnotation(inner.index, Middleware); get('/', (req, res) async { @@ -67,7 +70,7 @@ class HookedService extends Service { get( '/:id', (req, res) async => await this - .read(req.params['id'], mergeMap([req.query, restProvider])), + .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); @@ -76,7 +79,7 @@ class HookedService extends Service { patch( '/:id', (req, res) async => - await this.modify(req.params['id'], req.body, restProvider), + await this.modify(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -86,7 +89,7 @@ class HookedService extends Service { post( '/:id', (req, res) async => - await this.update(req.params['id'], req.body, restProvider), + await this.update(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -96,7 +99,7 @@ class HookedService extends Service { delete( '/:id', (req, res) async => await this - .remove(req.params['id'], mergeMap([req.query, restProvider])), + .remove(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 84ffedb2..4e9c281b 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -109,8 +109,19 @@ class ResponseContext extends Extensible { /// Redirects to the given named [Route]. 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); + _findRoute(Route route) { + for (Route child in route.children) { + final resolved = _findRoute(child); + + if (resolved != null) return resolved; + } + + return route.children + .firstWhere((r) => r.name == name, orElse: () => null); + } + + Route matched = _findRoute(app.root); + if (matched != null) { redirect(matched.makeUri(params), code: code); return; @@ -160,7 +171,8 @@ class ResponseContext extends Extensible { if (isOpen) { if (value is List) buffer.add(value); - else buffer.add(encoding.encode(value.toString())); + else + buffer.add(encoding.encode(value.toString())); } } } diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index ae1d844d..7a25fc7f 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -33,12 +33,10 @@ class Routable extends Router { final Map requestMiddleware = {}; /// A set of [Service] objects that have been mapped into routes. - Map get services => - new Map.unmodifiable(_services); + Map get services => _services; /// A set of [Controller] objects that have been loaded into the application. - Map get controllers => - new Map.unmodifiable(_controllers); + Map get controllers => _controllers; StreamController _onService = new StreamController.broadcast(); @@ -61,7 +59,7 @@ class Routable extends Router { @override Route addRoute(String method, Pattern path, Object handler, - {List middleware}) { + {List middleware: const []}) { final List handlers = []; // Merge @Middleware declaration, if any Middleware middlewareDeclaration = getAnnotation(handler, Middleware); @@ -69,8 +67,11 @@ class Routable extends Router { handlers.addAll(middlewareDeclaration.handlers); } - return super.addRoute(method, path, handler, - middleware: []..addAll(middleware ?? [])..addAll(handlers)); + final List handlerSequence = []; + handlerSequence.addAll(middleware ?? []); + handlerSequence.addAll(handlers); + + return super.addRoute(method, path, handler, middleware: handlerSequence); } void use(Pattern path, Router router, @@ -89,9 +90,11 @@ class Routable extends Router { .toString() .trim() .replaceAll(new RegExp(r'(^/+)|(/+$)'), '')] = service; + service.addRoutes(); } final handlers = []; + if (_router is AngelBase) { handlers.add((RequestContext req, ResponseContext res) async { req.app = _router; @@ -109,9 +112,9 @@ class Routable extends Router { copiedMiddleware[middlewareName]; } - root.child(path, debug: debug, handlers: handlers).addChild(router.root); - - _router.dumpTree(header: 'Mounting on "$path":'); + // _router.dumpTree(header: 'Mounting on "$path":'); + // root.child(path, debug: debug, handlers: handlers).addChild(router.root); + mount(path, _router); if (router is Routable) { // Copy services, too. :) diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 420fa01a..72c77ed4 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -98,8 +98,8 @@ class Angel extends AngelBase { /// Returns false on failure; otherwise, returns the HttpServer. Future startServer([InternetAddress address, int port]) async { final host = address ?? InternetAddress.LOOPBACK_IP_V4; - final server = await _serverGenerator(host, port ?? 0); - return this.httpServer = server..listen(handleRequest); + this.httpServer = await _serverGenerator(host, port ?? 0); + return httpServer..listen(handleRequest); } /// Loads some base dependencies into the service container. @@ -178,14 +178,13 @@ class Angel extends AngelBase { final req = await RequestContext.from(request, this); final res = new ResponseContext(request.response, this); String requestedUrl = request.uri - .toString() - .replaceAll("?" + request.uri.query, "") + .path .replaceAll(_straySlashes, ''); if (requestedUrl.isEmpty) requestedUrl = '/'; final route = resolve(requestedUrl, method: request.method); - print('Resolve ${requestedUrl} -> $route'); + _printDebug('Resolved ${requestedUrl} -> $route'); req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); final handlerSequence = []..addAll(before); @@ -331,8 +330,8 @@ class Angel extends AngelBase { bootstrapContainer(); _serverGenerator = (InternetAddress address, int port) async { var certificateChain = - Platform.script.resolve('server_chain.pem').toFilePath(); - var serverKey = Platform.script.resolve('server_key.pem').toFilePath(); + Platform.script.resolve(certificateChainPath).toFilePath(); + var serverKey = Platform.script.resolve(serverKeyPath).toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); serverContext.usePrivateKey(serverKey, diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index b04ddd4a..723f7ae6 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -65,14 +65,14 @@ class Service extends Routable { throw new AngelHttpException.MethodNotAllowed(); } - Service() : super() { + void addRoutes() { Map restProvider = {'provider': Providers.REST}; // Add global middleware if declared on the instance itself Middleware before = getAnnotation(this, Middleware); final handlers = []; - if (before != null) handlers.add(before.handlers); + if (before != null) handlers.addAll(before.handlers); Middleware indexMiddleware = getAnnotation(this.index, Middleware); get('/', (req, res) async { @@ -94,7 +94,7 @@ class Service extends Routable { get( '/:id', (req, res) async => await this - .read(req.params['id'], mergeMap([req.query, restProvider])), + .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); @@ -103,7 +103,7 @@ class Service extends Routable { patch( '/:id', (req, res) async => - await this.modify(req.params['id'], req.body, restProvider), + await this.modify(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -113,7 +113,7 @@ class Service extends Routable { post( '/:id', (req, res) async => - await this.update(req.params['id'], req.body, restProvider), + await this.update(req.params['id'], req.body, restProvider), middleware: [] ..addAll(handlers) ..addAll( @@ -123,10 +123,12 @@ class Service extends Routable { delete( '/:id', (req, res) async => await this - .remove(req.params['id'], mergeMap([req.query, restProvider])), + .remove(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( (removeMiddleware == null) ? [] : removeMiddleware.handlers)); + + normalize(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 0d1e5031..d5703a9d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,12 @@ name: angel_framework -version: 1.0.0-dev.22 +version: 1.0.0-dev.23 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework environment: sdk: ">=1.18.0" dependencies: - angel_route: - path: ../angel_route + angel_route: ^1.0.0-dev body_parser: ^1.0.0-dev container: ^0.1.2 json_god: ^2.0.0-beta diff --git a/test/controller.dart b/test/controller.dart index 8af37c83..2fb6e7bc 100644 --- a/test/controller.dart +++ b/test/controller.dart @@ -10,23 +10,23 @@ import 'common.dart'; class TodoController extends Controller { List todos = [new Todo(text: "Hello", over: "world")]; - @Expose("/:id", middleware: const["bar"]) - Future fetchTodo(int id, RequestContext req, - ResponseContext res) async { + @Expose("/:id", middleware: const ["bar"]) + Future fetchTodo( + int id, RequestContext req, ResponseContext res) async { expect(req, isNotNull); expect(res, isNotNull); return todos[id]; } @Expose("/namedRoute/:foo", as: "foo") - Future someRandomRoute(RequestContext req, - ResponseContext res) async { + Future someRandomRoute( + RequestContext req, ResponseContext res) async { return "${req.params['foo']}!"; } } main() { - Angel app = new Angel(); + Angel app; HttpServer server; InternetAddress host = InternetAddress.LOOPBACK_IP_V4; int port = 3000; @@ -34,25 +34,25 @@ main() { String url = "http://${host.address}:$port"; setUp(() async { + app = new Angel(); app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); app.registerMiddleware("bar", (req, res) async => res.write("world!")); - app.get("/redirect", (req, ResponseContext res) async => - res.redirectToAction("TodoController@foo", {"foo": "world"})); + app.get( + "/redirect", + (req, ResponseContext res) async => + res.redirectToAction("TodoController@foo", {"foo": "world"})); await app.configure(new TodoController()); print(app.controllers); - print("\nDUMPING ROUTES:"); - app.routes.forEach((Route route) { - print("\t${route.method} ${route.path} -> ${route.handlers}"); - }); - print("\n"); + app.dumpTree(); server = await app.startServer(host, port); client = new http.Client(); }); tearDown(() async { - await (server ?? app.httpServer).close(force: true); + await server.close(force: true); + app = null; client.close(); client = null; }); @@ -62,7 +62,7 @@ main() { var response = await client.get("$url/todos/0"); print(response.body); - expect(response.body.indexOf("Hello, "), equals(0)); + expect(rgx.firstMatch(response.body).start, equals(0)); Map todo = JSON.decode(response.body.replaceAll(rgx, "")); print("Todo: $todo"); diff --git a/test/di.dart b/test/di.dart index 8e2bdc22..bfc9d9af 100644 --- a/test/di.dart +++ b/test/di.dart @@ -15,7 +15,7 @@ main() { String url; setUp(() async { - app = new Angel(); + app = new Angel(debug: true); client = new http.Client(); // Inject some todos @@ -27,6 +27,7 @@ main() { await app.configure(new ErrandController()); server = await app.startServer(); + print('server: $server, httpServer: ${app.httpServer}'); url = "http://${server.address.host}:${server.port}"; }); diff --git a/test/hooked.dart b/test/hooked.dart index b72d7930..f9026d36 100644 --- a/test/hooked.dart +++ b/test/hooked.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; @@ -6,74 +7,86 @@ import 'common.dart'; main() { Map headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }; - Angel app; - String url; - http.Client client; - HookedService Todos; + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; - setUp(() async { - app = new Angel(); - client = new http.Client(); - app.use('/todos', new MemoryService()); - Todos = app.service("todos"); + Angel app; + HttpServer server; + String url; + http.Client client; + HookedService Todos; - await app.startServer(null, 0); - url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; - }); + setUp(() async { + app = new Angel(debug: true); + client = new http.Client(); + app.use('/todos', new MemoryService()); + Todos = app.service("todos"); - tearDown(() async { - app = null; - url = null; - client.close(); - client = null; - Todos = null; - }); + app + ..normalize() + ..dumpTree(showMatchers: true); - test("listen before and after", () async { - int count = 0; + server = await app.startServer(); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); - Todos - ..beforeIndexed.listen((_) { - count++; - }) - ..afterIndexed.listen((_) { - count++; - }); + tearDown(() async { + await server.close(force: true); + app = null; + url = null; + client.close(); + client = null; + Todos = null; + }); - var response = await client.get("$url/todos"); - print(response.body); - expect(count, equals(2)); - }); + test("listen before and after", () async { + int count = 0; - test("cancel before", () async { - Todos.beforeCreated..listen((HookedServiceEvent event) { + Todos + ..beforeIndexed.listen((_) { + count++; + }) + ..afterIndexed.listen((_) { + count++; + }); + + var response = await client.get("$url/todos"); + print(response.body); + expect(count, equals(2)); + }); + + test("cancel before", () async { + Todos.beforeCreated + ..listen((HookedServiceEvent event) { event.cancel({"hello": "hooked world"}); - })..listen((HookedServiceEvent event) { + }) + ..listen((HookedServiceEvent event) { event.cancel({"this_hook": "should never run"}); }); - var response = await client.post( - "$url/todos", body: god.serialize({"arbitrary": "data"}), - headers: headers); - print(response.body); - Map result = god.deserialize(response.body); - expect(result["hello"], equals("hooked world")); - }); + var response = await client.post("$url/todos", + body: god.serialize({"arbitrary": "data"}), headers: headers); + print(response.body); + Map result = god.deserialize(response.body); + expect(result["hello"], equals("hooked world")); + }); - test("cancel after", () async { - Todos.afterIndexed..listen((HookedServiceEvent event) async { + test("cancel after", () async { + Todos.afterIndexed + ..listen((HookedServiceEvent event) async { // Hooks can be Futures ;) - event.cancel([{"angel": "framework"}]); - })..listen((HookedServiceEvent event) { + event.cancel([ + {"angel": "framework"} + ]); + }) + ..listen((HookedServiceEvent event) { event.cancel({"this_hook": "should never run either"}); }); - var response = await client.get("$url/todos"); - print(response.body); - List result = god.deserialize(response.body); - expect(result[0]["angel"], equals("framework")); - }); -} \ No newline at end of file + var response = await client.get("$url/todos"); + print(response.body); + List result = god.deserialize(response.body); + expect(result[0]["angel"], equals("framework")); + }); +} diff --git a/test/routing.dart b/test/routing.dart index 6acb9a8f..d4249567 100644 --- a/test/routing.dart +++ b/test/routing.dart @@ -26,7 +26,7 @@ main() { http.Client client; setUp(() async { - final debug = false; + final debug = true; angel = new Angel(debug: debug); nested = new Angel(debug: debug); todos = new Angel(debug: debug); @@ -38,7 +38,7 @@ main() { }) ..registerMiddleware('intercept_service', (RequestContext req, res) async { - print("Intercepting a service!"); + res.write("Service with "); return true; }); @@ -48,7 +48,8 @@ main() { ted = nested.post('/ted/:route', (RequestContext req, res) { print('Params: ${req.params}'); - print('Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}'); + print( + 'Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}'); return req.params; }); @@ -75,7 +76,9 @@ main() { angel.use('/query', new QueryService()); angel.get('*', 'MJ'); - angel.dumpTree(header: "DUMPING ROUTES:"); + angel + ..normalize() + ..dumpTree(header: "DUMPING ROUTES:", showMatchers: true); client = new http.Client(); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); @@ -101,6 +104,7 @@ main() { var response = await client.get('$url/name/HELLO/last/WORLD'); print(response.body); var json = god.deserialize(response.body); + expect(json, new isInstanceOf>()); expect(json['first'], equals('HELLO')); expect(json['last'], equals('WORLD')); }); @@ -165,6 +169,6 @@ main() { response = await client.get("$url/query/foo?bar=baz"); print(response.body); - expect(response.body, equals("Middleware")); + expect(response.body, equals("Service with Middleware")); }); } From fc13db72ffd24660a2a1af93076dff9cb82c9873 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 04:16:21 -0500 Subject: [PATCH 054/414] Renamed tests --- test/all_tests.dart | 14 -------------- test/{controller.dart => controller_test.dart} | 0 test/{hooked.dart => hooked_test.dart} | 0 test/{routing.dart => routing_test.dart} | 0 test/{services.dart => services_test.dart} | 0 test/{util.dart => util_test.dart} | 0 6 files changed, 14 deletions(-) delete mode 100644 test/all_tests.dart rename test/{controller.dart => controller_test.dart} (100%) rename test/{hooked.dart => hooked_test.dart} (100%) rename test/{routing.dart => routing_test.dart} (100%) rename test/{services.dart => services_test.dart} (100%) rename test/{util.dart => util_test.dart} (100%) diff --git a/test/all_tests.dart b/test/all_tests.dart deleted file mode 100644 index 3b289eef..00000000 --- a/test/all_tests.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:test/test.dart'; -import 'controller.dart' as controller; -import 'di.dart' as di; -import 'hooked.dart' as hooked; -import 'routing.dart' as routing; -import 'services.dart' as services; - -main() { - group('controller', controller.main); - group('hooked', hooked.main); - group('di', di.main); - group('routing', routing.main); - group('services', services.main); -} \ No newline at end of file diff --git a/test/controller.dart b/test/controller_test.dart similarity index 100% rename from test/controller.dart rename to test/controller_test.dart diff --git a/test/hooked.dart b/test/hooked_test.dart similarity index 100% rename from test/hooked.dart rename to test/hooked_test.dart diff --git a/test/routing.dart b/test/routing_test.dart similarity index 100% rename from test/routing.dart rename to test/routing_test.dart diff --git a/test/services.dart b/test/services_test.dart similarity index 100% rename from test/services.dart rename to test/services_test.dart diff --git a/test/util.dart b/test/util_test.dart similarity index 100% rename from test/util.dart rename to test/util_test.dart From 0e5f315773b97e78b15915705d84ad71295344b6 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 14:50:17 -0500 Subject: [PATCH 055/414] Fixed a bunch of issues --- README.md | 2 +- lib/src/http/controller.dart | 25 ++++++++--- lib/src/http/request_context.dart | 37 +++++++++++----- lib/src/http/response_context.dart | 20 ++++++--- lib/src/http/server.dart | 71 +++++++++++++++++++----------- pubspec.yaml | 2 +- test/di.dart | 12 +++-- 7 files changed, 111 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index bb83bbe0..e041d96e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev.23-red.svg) +![version 1.0.0-dev.24](https://img.shields.io/badge/version-1.0.0--dev.24-red.svg) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index 7eb716ba..fbc37927 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -73,19 +73,30 @@ class Controller { var arg = req.params[name]; if (arg == null) { - if (parameter.type.reflectedType != dynamic) { + if (req.injections.containsKey(name)) { + args.add(req.injections[name]); + continue; + } + + final type = parameter.type.reflectedType; + + if (req.injections.containsKey(type)) { + args.add(req.injections[type]); + continue; + } + + if (type != dynamic) { try { - arg = app.container.make(parameter.type.reflectedType); - if (arg != null) { - args.add(arg); - continue; - } + args.add(app.container.make(type)); + continue; } catch (e) { // + print(e); + print(req.injections); } } - if (!exposeMirror.reflectee.allowNull.contain(name)) + if (!exposeMirror.reflectee.allowNull.contains(name)) throw new AngelHttpException.BadRequest( message: "Missing parameter '$name'"); } else diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart index af2544a3..a759d101 100644 --- a/lib/src/http/request_context.dart +++ b/lib/src/http/request_context.dart @@ -10,26 +10,32 @@ import 'angel_base.dart'; class RequestContext extends Extensible { BodyParseResult _body; ContentType _contentType; + HttpRequest _io; String _path; - HttpRequest _underlyingRequest; /// The [Angel] instance that is responding to this request. AngelBase app; /// Any cookies sent with this request. - List get cookies => underlyingRequest.cookies; + List get cookies => io.cookies; /// All HTTP headers sent with this request. - HttpHeaders get headers => underlyingRequest.headers; + HttpHeaders get headers => io.headers; /// The requested hostname. - String get hostname => underlyingRequest.headers.value(HttpHeaders.HOST); + String get hostname => io.headers.value(HttpHeaders.HOST); + + /// A [Map] of values that should be DI'd. + final Map injections = {}; + + /// The underlying [HttpRequest] instance underneath this context. + HttpRequest get io => _io; /// The user's IP. String get ip => remoteAddress.address; /// This request's HTTP method. - String get method => underlyingRequest.method; + String get method => io.method; /// All post data submitted to the server. Map get body => _body.body; @@ -51,24 +57,27 @@ class RequestContext extends Extensible { /// The remote address requesting this resource. InternetAddress get remoteAddress => - underlyingRequest.connectionInfo.remoteAddress; + io.connectionInfo.remoteAddress; /// The user's HTTP session. - HttpSession get session => underlyingRequest.session; + HttpSession get session => io.session; /// The [Uri] instance representing the path this request is responding to. - Uri get uri => underlyingRequest.uri; + Uri get uri => io.uri; /// Is this an **XMLHttpRequest**? bool get xhr => - underlyingRequest.headers + io.headers .value("X-Requested-With") ?.trim() ?.toLowerCase() == 'xmlhttprequest'; - /// The underlying [HttpRequest] instance underneath this context. - HttpRequest get underlyingRequest => _underlyingRequest; + @deprecated + HttpRequest get underlyingRequest { + throw new Exception( + '`RequestContext#underlyingRequest` is deprecated. Please update your application to use the newer `RequestContext#io`.'); + } /// Magically transforms an [HttpRequest] into a [RequestContext]. static Future from(HttpRequest request, AngelBase app) async { @@ -80,10 +89,14 @@ class RequestContext extends Extensible { .toString() .replaceAll("?" + request.uri.query, "") .replaceAll(new RegExp(r'/+$'), ''); - ctx._underlyingRequest = request; + ctx._io = request; ctx._body = await parseBody(request); return ctx; } + + void inject(Type type, value) { + injections[type] = value; + } } diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index 4e9c281b..fe175451 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -25,16 +25,22 @@ class ResponseContext extends Extensible { /// Sets the status code to be sent with this response. void status(int code) { - underlyingResponse.statusCode = code; + io.statusCode = code; } /// The underlying [HttpResponse] under this instance. - final HttpResponse underlyingResponse; + final HttpResponse io; - ResponseContext(this.underlyingResponse, this.app); + @deprecated + HttpResponse get underlyingRequest { + throw new Exception( + '`ResponseContext#underlyingResponse` is deprecated. Please update your application to use the newer `ResponseContext#io`.'); + } + + ResponseContext(this.io, this.app); /// Any and all cookies to be sent to the user. - List get cookies => underlyingResponse.cookies; + List get cookies => io.cookies; /// Set this to true if you will manually close the response. bool willCloseItself = false; @@ -57,9 +63,9 @@ class ResponseContext extends Extensible { /// Sets a response header to the given value, or retrieves its value. header(String key, [String value]) { if (value == null) - return underlyingResponse.headers[key]; + return io.headers[key]; else - underlyingResponse.headers.set(key, value); + io.headers.set(key, value); } /// Serializes JSON to the response. @@ -163,7 +169,7 @@ class ResponseContext extends Extensible { header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)); willCloseItself = true; - await file.openRead().pipe(underlyingResponse); + await file.openRead().pipe(io); } /// Writes data to the response. diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 72c77ed4..38a258f0 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -105,6 +105,8 @@ class Angel extends AngelBase { /// Loads some base dependencies into the service container. void bootstrapContainer() { container.singleton(this, as: AngelBase); + container.singleton(this, as: Routable); + container.singleton(this, as: Router); container.singleton(this); if (runtimeType != Angel) container.singleton(this, as: Angel); @@ -130,7 +132,7 @@ class Angel extends AngelBase { } if (handler is RawRequestHandler) { - var result = await handler(req.underlyingRequest); + var result = await handler(req.io); if (result is bool) return result == true; else if (result != null) { @@ -167,8 +169,8 @@ class Angel extends AngelBase { } res.willCloseItself = true; - res.underlyingResponse.write(god.serialize(handler)); - await res.underlyingResponse.close(); + res.io.write(god.serialize(handler)); + await res.io.close(); return false; } @@ -177,23 +179,34 @@ class Angel extends AngelBase { final req = await RequestContext.from(request, this); final res = new ResponseContext(request.response, this); - String requestedUrl = request.uri - .path - .replaceAll(_straySlashes, ''); + String requestedUrl = request.uri.path.replaceAll(_straySlashes, ''); if (requestedUrl.isEmpty) requestedUrl = '/'; - final route = resolve(requestedUrl, method: request.method); - _printDebug('Resolved ${requestedUrl} -> $route'); - req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); + final resolved = []; - final handlerSequence = []..addAll(before); - if (route != null) handlerSequence.addAll(route.handlerSequence); - handlerSequence.addAll(after); + if (requestedUrl == '/') { + resolved.add(root.indexRoute); + } else { + resolved.addAll(resolveAll(requestedUrl, method: request.method)); + final route = resolved.first; + req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); + req.inject(Match, route.match(requestedUrl)); + } - _printDebug('Handler sequence on $requestedUrl: $handlerSequence'); + final pipeline = []..addAll(before); - for (final handler in handlerSequence) { + if (resolved.isNotEmpty) { + for (final route in resolved) { + pipeline.addAll(route.handlerSequence); + } + } + + pipeline.addAll(after); + + _printDebug('Handler sequence on $requestedUrl: $pipeline'); + + for (final handler in pipeline) { try { _printDebug('Executing handler: $handler'); final result = await executeHandler(handler, req, res); @@ -249,8 +262,7 @@ class Angel extends AngelBase { // Run a function after injecting from service container Future runContained(Function handler, RequestContext req, ResponseContext res, - {Map namedParameters, - Map injecting}) async { + {Map namedParameters}) async { ClosureMirror closureMirror = reflect(handler); List args = []; @@ -261,9 +273,9 @@ class Angel extends AngelBase { args.add(res); else { // First, search to see if we can map this to a type - if (parameter.type.reflectedType != dynamic) { - args.add(container.make(parameter.type.reflectedType, - namedParameters: namedParameters, injecting: injecting)); + if (req.injections.containsKey(parameter.type.reflectedType)) { + args.add(req.injections[parameter.type.reflectedType]); + continue; } else { String name = MirrorSystem.getName(parameter.simpleName); @@ -273,7 +285,12 @@ class Angel extends AngelBase { args.add(req); else if (name == "res") args.add(res); - else { + else if (req.injections.containsKey(name)) + args.add(req.injections[name]); + else if (parameter.type.reflectedType != dynamic) { + args.add(container.make(parameter.type.reflectedType, + injecting: req.injections)); + } else { throw new Exception( "Cannot resolve parameter '$name' within handler."); } @@ -324,20 +341,22 @@ class Angel extends AngelBase { /// Provide paths to a certificate chain and server key (both .pem). /// If no password is provided, a random one will be generated upon running /// the server. - Angel.secure(String certificateChainPath, String serverKeyPath, - {bool debug: false, String password}) - : super(debug: debug) { - bootstrapContainer(); - _serverGenerator = (InternetAddress address, int port) async { + factory Angel.secure(String certificateChainPath, String serverKeyPath, + {bool debug: false, String password}) { + final app = new Angel(debug: debug); + + app._serverGenerator = (InternetAddress address, int port) async { var certificateChain = Platform.script.resolve(certificateChainPath).toFilePath(); var serverKey = Platform.script.resolve(serverKeyPath).toFilePath(); var serverContext = new SecurityContext(); serverContext.useCertificateChain(certificateChain); serverContext.usePrivateKey(serverKey, - password: password ?? _randomString(8)); + password: password ?? app._randomString(8)); return await HttpServer.bindSecure(address, port, serverContext); }; + + return app; } } diff --git a/pubspec.yaml b/pubspec.yaml index d5703a9d..e22d5e84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.23 +version: 1.0.0-dev.24 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/di.dart b/test/di.dart index bfc9d9af..7b8599e9 100644 --- a/test/di.dart +++ b/test/di.dart @@ -22,12 +22,12 @@ main() { app.container.singleton(new Todo(text: TEXT, over: OVER)); app.get("/errands", (Todo singleton) => singleton); - app.get("/errands3", (Errand singleton, Todo foo, RequestContext req) => singleton.text); + app.get("/errands3", + (Errand singleton, Todo foo, RequestContext req) => singleton.text); await app.configure(new SingletonController()); await app.configure(new ErrandController()); server = await app.startServer(); - print('server: $server, httpServer: ${app.httpServer}'); url = "http://${server.address.host}:${server.port}"; }); @@ -77,7 +77,11 @@ class SingletonController extends Controller { @Expose("/errands4") class ErrandController extends Controller { @Expose("/") - errand(Errand errand) => errand.text; + errand(Errand errand, Match match) { + expect(match, isNotNull); + print('Match: ${match.group(0)}'); + return errand.text; + } } class Errand { @@ -85,4 +89,4 @@ class Errand { String get text => todo.text; Errand(this.todo); -} \ No newline at end of file +} From 39d70e74a6650178bf344e6762bb3673974fa178 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 24 Nov 2016 02:12:53 -0500 Subject: [PATCH 056/414] All fall through --- README.md | 2 +- lib/src/http/server.dart | 13 ++++++------- lib/src/http/server_shelved.dart | 19 +++++++++++++++++++ lib/src/util.dart | 2 -- pubspec.yaml | 3 ++- test/controller_test.dart | 13 +++++-------- 6 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 lib/src/http/server_shelved.dart diff --git a/README.md b/README.md index e041d96e..54d218a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -![version 1.0.0-dev.24](https://img.shields.io/badge/version-1.0.0--dev.24-red.svg) +![version 1.0.0-dev.25](https://img.shields.io/badge/version-1.0.0--dev.25-red.svg) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 38a258f0..6d1bc003 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:math' show Random; import 'dart:mirrors'; import 'package:json_god/json_god.dart' as god; +import 'package:shelf/shelf.dart' as shelf; import 'angel_base.dart'; import 'angel_http_exception.dart'; import 'controller.dart'; @@ -13,6 +14,7 @@ import 'response_context.dart'; import 'routable.dart'; import 'service.dart'; export 'package:container/container.dart'; +part 'server_shelved.dart'; final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); @@ -33,8 +35,8 @@ class Angel extends AngelBase { 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); + + ServerGenerator _serverGenerator = HttpServer.bind; /// Fired after a request is processed. Always runs. Stream get afterProcessed => _afterProcessed.stream; @@ -183,12 +185,9 @@ class Angel extends AngelBase { if (requestedUrl.isEmpty) requestedUrl = '/'; - final resolved = []; + final resolved = resolveAll(requestedUrl, method: request.method); - if (requestedUrl == '/') { - resolved.add(root.indexRoute); - } else { - resolved.addAll(resolveAll(requestedUrl, method: request.method)); + if (resolved.isNotEmpty) { final route = resolved.first; req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); req.inject(Match, route.match(requestedUrl)); diff --git a/lib/src/http/server_shelved.dart b/lib/src/http/server_shelved.dart new file mode 100644 index 00000000..0f7c77a3 --- /dev/null +++ b/lib/src/http/server_shelved.dart @@ -0,0 +1,19 @@ +part of angel_framework.http.server; + + +// Todo: Shelf interop +class ShelvedAngel extends Angel { + shelf.Pipeline pipeline = new shelf.Pipeline(); + + ShelvedAngel({bool debug: false}) : super(debug: debug) {} + + @override + Future startServer([InternetAddress address, int port]) async { + /* final handler = pipeline.addHandler((shelf.Request req) { + // io.handleRequest() + });*/ + + return await super.startServer(address, port); + } + +} diff --git a/lib/src/util.dart b/lib/src/util.dart index ad614889..710391b8 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -21,6 +21,4 @@ getAnnotation(obj, Type T) { ClassMirror classMirror = reflectClass(obj.runtimeType); return matchingAnnotation(classMirror.metadata, T); } - - return null; } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index e22d5e84..38676067 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.24 +version: 1.0.0-dev.25 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework @@ -12,6 +12,7 @@ dependencies: json_god: ^2.0.0-beta merge_map: ^1.0.0 mime: ^0.9.3 + shelf: ^0.6.7 dev_dependencies: http: ^0.11.3 test: ^0.12.13 diff --git a/test/controller_test.dart b/test/controller_test.dart index 2fb6e7bc..4490b3ea 100644 --- a/test/controller_test.dart +++ b/test/controller_test.dart @@ -28,10 +28,8 @@ class TodoController extends Controller { main() { Angel app; HttpServer server; - InternetAddress host = InternetAddress.LOOPBACK_IP_V4; - int port = 3000; - http.Client client; - String url = "http://${host.address}:$port"; + http.Client client = new http.Client(); + String url; setUp(() async { app = new Angel(); @@ -46,15 +44,14 @@ main() { print(app.controllers); app.dumpTree(); - server = await app.startServer(host, port); - client = new http.Client(); + server = await app.startServer(); + url = 'http://${server.address.address}:${server.port}'; }); tearDown(() async { await server.close(force: true); app = null; - client.close(); - client = null; + url = null; }); test("middleware", () async { From 774edb4be80fb8de6a7240f98fb6aeba49997594 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 27 Nov 2016 19:49:27 -0500 Subject: [PATCH 057/414] New router... Again --- README.md | 2 +- lib/src/http/response_context.dart | 37 +++++++++++++++++------- lib/src/http/routable.dart | 5 ++-- lib/src/http/server.dart | 32 ++++++++++++--------- lib/src/http/service.dart | 2 -- pubspec.yaml | 2 +- test/controller_test.dart | 11 ++++--- test/hooked_test.dart | 4 +-- test/routing_test.dart | 46 ++++++++++++++---------------- 9 files changed, 78 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 54d218a8..ca6e9370 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -![version 1.0.0-dev.25](https://img.shields.io/badge/version-1.0.0--dev.25-red.svg) +![version 1.0.0-dev.26](https://img.shields.io/badge/version-1.0.0--dev.26-red.svg) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart index fe175451..c3e870da 100644 --- a/lib/src/http/response_context.dart +++ b/lib/src/http/response_context.dart @@ -10,6 +10,8 @@ import '../extensible.dart'; import 'angel_base.dart'; import 'controller.dart'; +final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); + /// A convenience wrapper around an outgoing HTTP request. class ResponseContext extends Extensible { bool _isOpen = true; @@ -90,8 +92,15 @@ class ResponseContext extends Extensible { } /// Redirects to user to the given URL. - void redirect(String url, {int code: 301}) { - header(HttpHeaders.LOCATION, url); + /// + /// [url] can be a `String`, or a `List`. + /// If it is a `List`, a URI will be constructed + /// based on the provided params. + /// + /// See [Router]#navigate for more. :) + void redirect(url, {bool absolute: true, int code: 301}) { + header(HttpHeaders.LOCATION, + url is String ? url : app.navigate(url, absolute: absolute)); status(code ?? 301); write(''' @@ -115,18 +124,19 @@ class ResponseContext extends Extensible { /// Redirects to the given named [Route]. void redirectTo(String name, [Map params, int code]) { - _findRoute(Route route) { - for (Route child in route.children) { - final resolved = _findRoute(child); + Route _findRoute(Router r) { + for (Route route in r.routes) { + if (route is SymlinkRoute) { + final m = _findRoute(route.router); - if (resolved != null) return resolved; + if (m != null) return m; + } else if (route.name == name) return route; } - return route.children - .firstWhere((r) => r.name == name, orElse: () => null); + return null; } - Route matched = _findRoute(app.root); + Route matched = _findRoute(app); if (matched != null) { redirect(matched.makeUri(params), code: code); @@ -146,7 +156,8 @@ class ResponseContext extends Extensible { throw new Exception( "Controller redirects must take the form of 'Controller@action'. You gave: $action"); - Controller controller = app.controller(split[0]); + Controller controller = + app.controller(split[0].replaceAll(_straySlashes, '')); if (controller == null) throw new Exception("Could not find a controller named '${split[0]}'"); @@ -157,7 +168,11 @@ class ResponseContext extends Extensible { throw new Exception( "Controller '${split[0]}' does not contain any action named '${split[1]}'"); - redirect(matched.makeUri(params), code: code); + final head = + controller.exposeDecl.path.toString().replaceAll(_straySlashes, ''); + final tail = matched.makeUri(params).replaceAll(_straySlashes, ''); + + redirect('$head/$tail'.replaceAll(_straySlashes, ''), code: code); } /// Streams a file to this response as chunked data. diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 7a25fc7f..2bc7f104 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -11,6 +11,7 @@ import 'metadata.dart'; import 'request_context.dart'; import 'response_context.dart'; import 'service.dart'; +final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); /// A function that intercepts a request and determines whether handling of it should continue. typedef Future RequestMiddleware(RequestContext req, ResponseContext res); @@ -23,7 +24,7 @@ typedef Future RawRequestHandler(HttpRequest request); /// A routable server that can handle dynamic requests. class Routable extends Router { - final Map _controllers = {}; + final Map _controllers = {}; final Map _services = {}; Routable({bool debug: false}) : super(debug: debug); @@ -36,7 +37,7 @@ class Routable extends Router { Map get services => _services; /// A set of [Controller] objects that have been loaded into the application. - Map get controllers => _controllers; + Map get controllers => _controllers; StreamController _onService = new StreamController.broadcast(); diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart index 6d1bc003..5efb940b 100644 --- a/lib/src/http/server.dart +++ b/lib/src/http/server.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' show Random; import 'dart:mirrors'; +import 'package:angel_route/angel_route.dart'; import 'package:json_god/json_god.dart' as god; import 'package:shelf/shelf.dart' as shelf; import 'angel_base.dart'; @@ -185,23 +186,18 @@ class Angel extends AngelBase { if (requestedUrl.isEmpty) requestedUrl = '/'; - final resolved = resolveAll(requestedUrl, method: request.method); + final resolved = + resolveAll(requestedUrl, requestedUrl, method: request.method); + + for (final result in resolved) req.params.addAll(result.allParams); if (resolved.isNotEmpty) { - final route = resolved.first; - req.params.addAll(route?.parseParameters(requestedUrl) ?? {}); + final route = resolved.first.route; req.inject(Match, route.match(requestedUrl)); } - final pipeline = []..addAll(before); - - if (resolved.isNotEmpty) { - for (final route in resolved) { - pipeline.addAll(route.handlerSequence); - } - } - - pipeline.addAll(after); + final m = new MiddlewarePipeline(resolved); + final pipeline = []..addAll(before)..addAll(m.handlers)..addAll(after); _printDebug('Handler sequence on $requestedUrl: $pipeline'); @@ -304,7 +300,8 @@ class Angel extends AngelBase { Future configure(AngelConfigurer configurer) async { await configurer(this); - if (configurer is Controller) _onController.add(configurer); + if (configurer is Controller) + _onController.add(controllers[configurer.exposeDecl.path] = configurer); } /// Fallback when an error is thrown while handling a request. @@ -320,6 +317,15 @@ class Angel extends AngelBase { @override use(Pattern path, Routable routable, {bool hooked: true, String namespace: null}) { + if (routable is Angel) { + final head = path.toString().replaceAll(_straySlashes, ''); + + routable.controllers.forEach((k, v) { + final tail = k.toString().replaceAll(_straySlashes, ''); + controllers['$head/$tail'.replaceAll(_straySlashes, '')] = v; + }); + } + if (routable is Service) { routable.app = this; } diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index 723f7ae6..c286c09e 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -128,7 +128,5 @@ class Service extends Routable { ..addAll(handlers) ..addAll( (removeMiddleware == null) ? [] : removeMiddleware.handlers)); - - normalize(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 38676067..883ce189 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.25 +version: 1.0.0-dev.26 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/controller_test.dart b/test/controller_test.dart index 4490b3ea..dc63472f 100644 --- a/test/controller_test.dart +++ b/test/controller_test.dart @@ -12,10 +12,10 @@ class TodoController extends Controller { @Expose("/:id", middleware: const ["bar"]) Future fetchTodo( - int id, RequestContext req, ResponseContext res) async { + String id, RequestContext req, ResponseContext res) async { expect(req, isNotNull); expect(res, isNotNull); - return todos[id]; + return todos[int.parse(id)]; } @Expose("/namedRoute/:foo", as: "foo") @@ -32,7 +32,7 @@ main() { String url; setUp(() async { - app = new Angel(); + app = new Angel(debug: true); app.registerMiddleware("foo", (req, res) async => res.write("Hello, ")); app.registerMiddleware("bar", (req, res) async => res.write("world!")); app.get( @@ -57,7 +57,7 @@ main() { test("middleware", () async { var rgx = new RegExp("^Hello, world!"); var response = await client.get("$url/todos/0"); - print(response.body); + print('Response: ${response.body}'); expect(rgx.firstMatch(response.body).start, equals(0)); @@ -70,8 +70,7 @@ main() { test("named actions", () async { var response = await client.get("$url/redirect"); - print(response.body); - + print('Response: ${response.body}'); expect(response.body, equals("Hello, \"world!\"")); }); } diff --git a/test/hooked_test.dart b/test/hooked_test.dart index f9026d36..42ea966b 100644 --- a/test/hooked_test.dart +++ b/test/hooked_test.dart @@ -23,9 +23,7 @@ main() { app.use('/todos', new MemoryService()); Todos = app.service("todos"); - app - ..normalize() - ..dumpTree(showMatchers: true); + app.dumpTree(showMatchers: true); server = await app.startServer(); url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; diff --git a/test/routing_test.dart b/test/routing_test.dart index d4249567..9a302949 100644 --- a/test/routing_test.dart +++ b/test/routing_test.dart @@ -19,7 +19,7 @@ class QueryService extends Service { } main() { - Angel angel; + Angel app; Angel nested; Angel todos; String url; @@ -27,11 +27,11 @@ main() { setUp(() async { final debug = true; - angel = new Angel(debug: debug); + app = new Angel(debug: debug); nested = new Angel(debug: debug); todos = new Angel(debug: debug); - angel + app ..registerMiddleware('interceptor', (req, res) async { res.write('Middleware'); return false; @@ -53,41 +53,39 @@ main() { return req.params; }); - angel.use('/nes', nested); - angel.get('/meta', testMiddlewareMetadata); - angel.get('/intercepted', 'This should not be shown', + app.use('/nes', nested); + app.get('/meta', testMiddlewareMetadata); + app.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('/todos/:id', todos); - angel + app.get('/hello', 'world'); + app.get('/name/:first/last/:last', (req, res) => req.params); + app.post('/lambda', (req, res) => req.body); + app.use('/todos/:id', todos); + app .get('/greet/:name', (RequestContext req, res) async => "Hello ${req.params['name']}") .as('Named routes'); - angel.get('/named', (req, ResponseContext res) async { + app.get('/named', (req, ResponseContext res) async { res.redirectTo('Named routes', {'name': 'tests'}); }); - angel.get('/log', (RequestContext req, res) async { + app.get('/log', (RequestContext req, res) async { print("Query: ${req.query}"); return "Logged"; }); - angel.use('/query', new QueryService()); - angel.get('*', 'MJ'); + app.use('/query', new QueryService()); + app.get('*', 'MJ'); - angel - ..normalize() - ..dumpTree(header: "DUMPING ROUTES:", showMatchers: true); + app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true); client = new http.Client(); - await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); - url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; }); tearDown(() async { - await angel.httpServer.close(force: true); - angel = null; + await app.httpServer.close(force: true); + app = null; nested = null; todos = null; client.close(); @@ -102,7 +100,7 @@ main() { test('Can match url with multiple parameters', () async { var response = await client.get('$url/name/HELLO/last/WORLD'); - print(response.body); + print('Response: ${response.body}'); var json = god.deserialize(response.body); expect(json, new isInstanceOf>()); expect(json['first'], equals('HELLO')); @@ -119,7 +117,7 @@ main() { 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['id'], equals('1337')); expect(json['action'], equals('test')); }); From 93ea5bf6f1264a0d6c5becadfddd3c3b85fda557 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Mon, 28 Nov 2016 16:48:00 -0500 Subject: [PATCH 058/414] +27 --- README.md | 2 +- lib/src/http/controller.dart | 15 ++++++++------- pubspec.yaml | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ca6e9370..402c8706 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -![version 1.0.0-dev.26](https://img.shields.io/badge/version-1.0.0--dev.26-red.svg) +![version 1.0.0-dev.27](https://img.shields.io/badge/version-1.0.0--dev.27-red.svg) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart index fbc37927..c575215b 100644 --- a/lib/src/http/controller.dart +++ b/lib/src/http/controller.dart @@ -34,7 +34,11 @@ class Controller { "All controllers must carry an @Expose() declaration."); } - app.use(exposeDecl.path, generateRoutable(classMirror)); + + final routable = new Routable(debug: true); + configureRoutes(routable); + + app.use(exposeDecl.path, routable); TypeMirror typeMirror = reflectType(this.runtimeType); String name = exposeDecl.as; @@ -125,14 +129,11 @@ class Controller { }; } - Routable generateRoutable(ClassMirror classMirror) { - final routable = new Routable(debug: true); - final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware); - + void configureRoutes(Routable routable) { + ClassMirror classMirror = reflectClass(this.runtimeType); InstanceMirror instanceMirror = reflect(this); + final handlers = []..addAll(exposeDecl.middleware)..addAll(middleware); final callback = _callback(instanceMirror, routable, handlers); classMirror.instanceMembers.forEach(callback); - - return routable; } } diff --git a/pubspec.yaml b/pubspec.yaml index 883ce189..eb2f5260 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.26 +version: 1.0.0-dev.27 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From b41ee89ebf0e2f6bc240916eec615169b04be44c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 3 Dec 2016 11:32:20 -0500 Subject: [PATCH 059/414] 28 --- README.md | 2 +- lib/src/http/hooked_service.dart | 6 +++--- lib/src/http/service.dart | 6 +++--- pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 402c8706..e1a1298a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -![version 1.0.0-dev.27](https://img.shields.io/badge/version-1.0.0--dev.27-red.svg) +![version 1.0.0-dev.28](https://img.shields.io/badge/version-1.0.0--dev.28-red.svg) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart index 3f7552f8..22d7c8e1 100644 --- a/lib/src/http/hooked_service.dart +++ b/lib/src/http/hooked_service.dart @@ -59,7 +59,7 @@ class HookedService extends Service { ..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers)); Middleware createMiddleware = getAnnotation(inner.create, Middleware); - post('/', (req, res) async => await this.create(req.body, restProvider), + post('/', (req, res) async => await this.create(req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -79,7 +79,7 @@ class HookedService extends Service { patch( '/:id', (req, res) async => - await this.modify(req.params['id'], req.body, restProvider), + await this.modify(req.params['id'], req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -89,7 +89,7 @@ class HookedService extends Service { post( '/:id', (req, res) async => - await this.update(req.params['id'], req.body, restProvider), + await this.update(req.params['id'], req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart index c286c09e..d7f6e1a0 100644 --- a/lib/src/http/service.dart +++ b/lib/src/http/service.dart @@ -83,7 +83,7 @@ class Service extends Routable { ..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers)); Middleware createMiddleware = getAnnotation(this.create, Middleware); - post('/', (req, res) async => await this.create(req.body, restProvider), + post('/', (req, res) async => await this.create(req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -103,7 +103,7 @@ class Service extends Routable { patch( '/:id', (req, res) async => - await this.modify(req.params['id'], req.body, restProvider), + await this.modify(req.params['id'], req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -113,7 +113,7 @@ class Service extends Routable { post( '/:id', (req, res) async => - await this.update(req.params['id'], req.body, restProvider), + await this.update(req.params['id'], req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( diff --git a/pubspec.yaml b/pubspec.yaml index eb2f5260..a4f53d34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.27 +version: 1.0.0-dev.28 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From e00fc57aff6310563b47f538750d72f9b3725016 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 8 Dec 2016 17:46:23 -0500 Subject: [PATCH 060/414] Re-designed exception --- .gitignore | 1 - .idea/encodings.xml | 6 + .idea/libraries/Dart_Packages.xml | 403 +++++++++++++++++++++++++ .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 366 ++++++++++++++++++++++ README.md | 2 +- lib/src/http/angel_http_exception.dart | 121 ++++---- pubspec.yaml | 2 +- 10 files changed, 861 insertions(+), 58 deletions(-) create mode 100644 .idea/encodings.xml create mode 100644 .idea/libraries/Dart_Packages.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml diff --git a/.gitignore b/.gitignore index 951d3784..fa1e2750 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ *.iml ## Directory-based project format: -.idea/ # if you remove the above rule, at least ignore the following: # User-specific stuff: diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..97626ba4 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml new file mode 100644 index 00000000..299eecf0 --- /dev/null +++ b/.idea/libraries/Dart_Packages.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..1719e444 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..fd63bed5 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 00000000..17424235 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + DEFINITION_ORDER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project + + + true + + + + DIRECTORY + + false + + + + + + + + + 1481236071442 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index e1a1298a..8251d0a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -![version 1.0.0-dev.28](https://img.shields.io/badge/version-1.0.0--dev.28-red.svg) +[![pub 1.0.0-dev.29](https://img.shields.io/badge/pub-1.0.0--dev.29-red.svg)](https://pub.dartlang.org/packages/angel_framework) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/angel_http_exception.dart b/lib/src/http/angel_http_exception.dart index 3e91b312..d8136d42 100644 --- a/lib/src/http/angel_http_exception.dart +++ b/lib/src/http/angel_http_exception.dart @@ -1,21 +1,30 @@ library angel_framework.http.angel_http_exception; -class _AngelHttpExceptionBase implements Exception { - /// An HTTP status code this exception will throw. - int statusCode; +/// Basically the same as +/// [feathers-errors](https://github.com/feathersjs/feathers-errors). +class AngelHttpException { + var error; + + /// A list of errors that occurred when this exception was thrown. + final List errors = []; + /// The cause of this exception. String message; - /// A list of errors that occurred when this exception was thrown. - List errors; - _AngelHttpExceptionBase.base() {} + /// The [StackTrace] associated with this error. + StackTrace stackTrace; - _AngelHttpExceptionBase(this.statusCode, this.message, - {List this.errors: const []}); + /// An HTTP status code this exception will throw. + int statusCode; - @override - String toString() { - return "$statusCode: $message"; + AngelHttpException(this.error, + {this.message: '500 Internal Server Error', + this.stackTrace, + this.statusCode: 500, + List errors: const []}) { + if (errors != null) { + this.errors.addAll(errors); + } } Map toJson() { @@ -28,74 +37,76 @@ class _AngelHttpExceptionBase implements Exception { } Map toMap() => toJson(); -} -/// Basically the same as -/// [feathers-errors](https://github.com/feathersjs/feathers-errors). -class AngelHttpException extends _AngelHttpExceptionBase { - /// Throws a 500 Internal Server Error. - /// Set includeRealException to true to print include the actual exception along with - /// this error. Useful flag for development vs. production. - AngelHttpException(error, - {bool includeRealError: false, StackTrace stackTrace}) :super.base() { - statusCode = 500; - message = "500 Internal Server Error"; - if (includeRealError) { - errors.add(error.toString()); - if (stackTrace != null) { - errors.add(stackTrace.toString()); - } - } + @override + String toString() { + return "$statusCode: $message"; } + factory AngelHttpException.fromMap(Map data) { + return new AngelHttpException(null, + statusCode: data['statusCode'], + message: data['message'], + errors: data['errors']); + } + + factory AngelHttpException.fromJson(String json) => + new AngelHttpException.fromMap(JSON.decode(json)); + /// Throws a 400 Bad Request error, including an optional arrray of (validation?) /// errors you specify. - AngelHttpException.BadRequest( - {String message: '400 Bad Request', List errors: const[]}) - : super(400, message, errors: errors); + factory AngelHttpException.BadRequest( + {String message: '400 Bad Request', List errors: const []}) => + new AngelHttpException(null, + message: message, errors: errors, statusCode: 400); /// Throws a 401 Not Authenticated error. - AngelHttpException.NotAuthenticated({String message: '401 Not Authenticated'}) - : super(401, message); + factory AngelHttpException.NotAuthenticated( + {String message: '401 Not Authenticated'}) => + new AngelHttpException(null, message: message, statusCode: 401); /// Throws a 402 Payment Required error. - AngelHttpException.PaymentRequired({String message: '402 Payment Required'}) - : super(402, message); + factory AngelHttpException.PaymentRequired( + {String message: '402 Payment Required'}) => + new AngelHttpException(null, message: message, statusCode: 402); /// Throws a 403 Forbidden error. - AngelHttpException.Forbidden({String message: '403 Forbidden'}) - : super(403, message); + factory AngelHttpException.Forbidden({String message: '403 Forbidden'}) => + new AngelHttpException(null, message: message, statusCode: 403); /// Throws a 404 Not Found error. - AngelHttpException.NotFound({String message: '404 Not Found'}) - : super(404, message); + factory AngelHttpException.NotFound({String message: '404 Not Found'}) => + new AngelHttpException(null, message: message, statusCode: 404); /// Throws a 405 Method Not Allowed error. - AngelHttpException.MethodNotAllowed( - {String message: '405 Method Not Allowed'}) - : super(405, message); + factory AngelHttpException.MethodNotAllowed( + {String message: '405 Method Not Allowed'}) => + new AngelHttpException(null, message: message, statusCode: 405); /// Throws a 406 Not Acceptable error. - AngelHttpException.NotAcceptable({String message: '406 Not Acceptable'}) - : super(406, message); + factory AngelHttpException.NotAcceptable( + {String message: '406 Not Acceptable'}) => + new AngelHttpException(null, message: message, statusCode: 406); /// Throws a 408 Timeout error. - AngelHttpException.MethodTimeout({String message: '408 Timeout'}) - : super(408, message); + factory AngelHttpException.MethodTimeout({String message: '408 Timeout'}) => + new AngelHttpException(null, message: message, statusCode: 408); /// Throws a 409 Conflict error. - AngelHttpException.Conflict({String message: '409 Conflict'}) - : super(409, message); + factory AngelHttpException.Conflict({String message: '409 Conflict'}) => + new AngelHttpException(null, message: message, statusCode: 409); /// Throws a 422 Not Processable error. - AngelHttpException.NotProcessable({String message: '422 Not Processable'}) - : super(422, message); + factory AngelHttpException.NotProcessable( + {String message: '422 Not Processable'}) => + new AngelHttpException(null, message: message, statusCode: 422); /// Throws a 501 Not Implemented error. - AngelHttpException.NotImplemented({String message: '501 Not Implemented'}) - : super(501, message); + factory AngelHttpException.NotImplemented( + {String message: '501 Not Implemented'}) => + new AngelHttpException(null, message: message, statusCode: 501); /// Throws a 503 Unavailable error. - AngelHttpException.Unavailable({String message: '503 Unavailable'}) - : super(503, message); -} \ No newline at end of file + factory AngelHttpException.Unavailable({String message: '503 Unavailable'}) => + new AngelHttpException(null, message: message, statusCode: 503); +} diff --git a/pubspec.yaml b/pubspec.yaml index a4f53d34..2d25e4cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.28 +version: 1.0.0-dev.29 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From bf3b77c7ab08ff7da7843157e705e49ec3b633ea Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 8 Dec 2016 18:04:33 -0500 Subject: [PATCH 061/414] Re-designed exception --- .idea/workspace.xml | 160 +++++++++++++++++-------- README.md | 2 +- lib/src/http/angel_http_exception.dart | 4 +- pubspec.yaml | 2 +- 4 files changed, 115 insertions(+), 53 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 17424235..6568d145 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,13 +2,7 @@ - - - - - - - + @@ -31,17 +25,40 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -52,11 +69,11 @@ - + - - + + @@ -107,7 +124,6 @@ - @@ -177,6 +193,7 @@ + @@ -234,6 +251,12 @@ + project + + + + + @@ -267,37 +290,46 @@ - - - - - - - - - - + + + + + + - - + + - + - + + + + @@ -306,23 +338,19 @@ + + + - - - - - - - - - + @@ -335,10 +363,50 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + @@ -346,21 +414,13 @@ - - + + - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 8251d0a5..1fd13365 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -[![pub 1.0.0-dev.29](https://img.shields.io/badge/pub-1.0.0--dev.29-red.svg)](https://pub.dartlang.org/packages/angel_framework) +[![pub 1.0.0-dev.30](https://img.shields.io/badge/pub-1.0.0--dev.30-red.svg)](https://pub.dartlang.org/packages/angel_framework) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/angel_http_exception.dart b/lib/src/http/angel_http_exception.dart index d8136d42..072b716e 100644 --- a/lib/src/http/angel_http_exception.dart +++ b/lib/src/http/angel_http_exception.dart @@ -1,8 +1,10 @@ library angel_framework.http.angel_http_exception; +import 'dart:convert'; + /// Basically the same as /// [feathers-errors](https://github.com/feathersjs/feathers-errors). -class AngelHttpException { +class AngelHttpException implements Exception { var error; /// A list of errors that occurred when this exception was thrown. diff --git a/pubspec.yaml b/pubspec.yaml index 2d25e4cd..6081107b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 1.0.0-dev.29 +version: 1.0.0-dev.30 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework From 9047890f527a15b25de98d0203ffb74adc62191a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 10 Dec 2016 09:05:40 -0500 Subject: [PATCH 062/414] Core done? --- .idea/libraries/Dart_Packages.xml | 196 +++++++++++------------ .idea/modules.xml | 2 +- .idea/workspace.xml | 248 +++++++++++++++++++++--------- README.md | 2 +- lib/src/http/hooked_service.dart | 54 ++++++- lib/src/http/metadata.dart | 25 ++- lib/src/http/routable.dart | 1 + pubspec.yaml | 4 +- test/common.dart | 15 +- test/hooked_test.dart | 12 +- 10 files changed, 371 insertions(+), 188 deletions(-) diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 299eecf0..36d08e55 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -5,398 +5,398 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml index fd63bed5..406d4377 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 6568d145..9bf5a491 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,11 +1,17 @@ - + + + - + + + + + @@ -26,35 +32,52 @@ - - - - - - - - - - - - + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -72,8 +95,8 @@ - - + + @@ -81,6 +104,11 @@ + + + BEFORE + + @@ -89,6 +117,10 @@ @@ -101,10 +133,10 @@ DEFINITION_ORDER - @@ -124,25 +156,27 @@ + + - - - - @@ -152,11 +186,11 @@ - - @@ -170,11 +204,11 @@ - - @@ -192,8 +226,6 @@ - - @@ -208,18 +240,7 @@ - - - + @@ -279,6 +300,13 @@ false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - + - - + + + + + - - - @@ -340,13 +401,30 @@ - + + + + + + + + + + + + + + + + @@ -389,24 +467,48 @@ - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -414,13 +516,21 @@ - - + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 1fd13365..90753e44 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_framework -[![pub 1.0.0-dev.30](https://img.shields.io/badge/pub-1.0.0--dev.30-red.svg)](https://pub.dartlang.org/packages/angel_framework) +[![pub 1.0.0-dev.31](https://img.shields.io/badge/pub-1.0.0--dev.31-red.svg)](https://pub.dartlang.org/packages/angel_framework) ![build status](https://travis-ci.org/angel-dart/framework.svg) Core libraries for the Angel Framework. \ No newline at end of file diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart index 22d7c8e1..2e5eccdb 100644 --- a/lib/src/http/hooked_service.dart +++ b/lib/src/http/hooked_service.dart @@ -39,6 +39,41 @@ class HookedService extends Service { if (inner.app != null) this.app = inner.app; } + void addHooks() { + Hooks hooks = getAnnotation(inner, Hooks); + final before = []; + final after = []; + + if (hooks != null) { + before.addAll(hooks.before); + after.addAll(hooks.after); + } + + void applyListeners(Function fn, HookedServiceEventDispatcher dispatcher, + [bool isAfter]) { + Hooks hooks = getAnnotation(fn, Hooks); + final listeners = []..addAll(isAfter == true ? after : before); + + if (hooks != null) + listeners.addAll(isAfter == true ? hooks.after : hooks.before); + + listeners.forEach(dispatcher.listen); + } + + applyListeners(inner.index, beforeIndexed); + applyListeners(inner.read, beforeRead); + applyListeners(inner.created, beforeCreated); + applyListeners(inner.modify, beforeModified); + applyListeners(inner.updated, beforeUpdated); + applyListeners(inner.removed, beforeRemoved); + applyListeners(inner.index, afterIndexed, true); + applyListeners(inner.read, afterRead, true); + applyListeners(inner.created, afterCreated, true); + applyListeners(inner.modify, afterModified, true); + applyListeners(inner.updated, afterUpdated, true); + applyListeners(inner.removed, afterRemoved, true); + } + @override void addRoutes() { // Set up our routes. We still need to copy middleware from inner service @@ -59,7 +94,10 @@ class HookedService extends Service { ..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers)); Middleware createMiddleware = getAnnotation(inner.create, Middleware); - post('/', (req, res) async => await this.create(req.body, mergeMap([req.query, restProvider])), + post( + '/', + (req, res) async => + await this.create(req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -70,7 +108,7 @@ class HookedService extends Service { get( '/:id', (req, res) async => await this - .read(req.params['id'], mergeMap([req.query, restProvider])), + .read(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll((readMiddleware == null) ? [] : readMiddleware.handlers)); @@ -78,8 +116,8 @@ class HookedService extends Service { Middleware modifyMiddleware = getAnnotation(inner.modify, Middleware); patch( '/:id', - (req, res) async => - await this.modify(req.params['id'], req.body, mergeMap([req.query, restProvider])), + (req, res) async => await this.modify( + req.params['id'], req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -88,8 +126,8 @@ class HookedService extends Service { Middleware updateMiddleware = getAnnotation(inner.update, Middleware); post( '/:id', - (req, res) async => - await this.update(req.params['id'], req.body, mergeMap([req.query, restProvider])), + (req, res) async => await this.update( + req.params['id'], req.body, mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( @@ -99,11 +137,13 @@ class HookedService extends Service { delete( '/:id', (req, res) async => await this - .remove(req.params['id'], mergeMap([req.query, restProvider])), + .remove(req.params['id'], mergeMap([req.query, restProvider])), middleware: [] ..addAll(handlers) ..addAll( (removeMiddleware == null) ? [] : removeMiddleware.handlers)); + + addHooks(); } @override diff --git a/lib/src/http/metadata.dart b/lib/src/http/metadata.dart index 1e96c449..194b0306 100644 --- a/lib/src/http/metadata.dart +++ b/lib/src/http/metadata.dart @@ -1,10 +1,12 @@ library angel_framework.http.metadata; +import 'hooked_service.dart' show HookedServiceEventListener; + /// Annotation to map middleware onto a handler. class Middleware { final List handlers; - const Middleware(List this.handlers); + const Middleware(this.handlers); } /// Annotation to set a service up to release hooks on every action. @@ -12,6 +14,15 @@ class Hooked { const Hooked(); } +/// Attaches hooks to a [HookedService]. +class Hooks { + final List before; + final List after; + + const Hooks({this.before: const [], this.after: const []}); +} + +/// Exposes a [Controller] to the Internet. class Expose { final String method; final Pattern path; @@ -19,9 +30,9 @@ class Expose { final String as; final List allowNull; - const Expose(Pattern this.path, - {String this.method: "GET", - List this.middleware: const [], - String this.as: null, - List this.allowNull: const[]}); -} \ No newline at end of file + const Expose(this.path, + {this.method: "GET", + this.middleware: const [], + this.as: null, + this.allowNull: const []}); +} diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart index 2bc7f104..16c3ef87 100644 --- a/lib/src/http/routable.dart +++ b/lib/src/http/routable.dart @@ -11,6 +11,7 @@ import 'metadata.dart'; import 'request_context.dart'; import 'response_context.dart'; import 'service.dart'; + final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); /// A function that intercepts a request and determines whether handling of it should continue. diff --git a/pubspec.yaml b/pubspec.yaml index 6081107b..dacc6765 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: angel_framework -version: 1.0.0-dev.30 +version: 1.0.0-dev.31 description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework environment: - sdk: ">=1.18.0" + sdk: ">=1.19.0" dependencies: angel_route: ^1.0.0-dev body_parser: ^1.0.0-dev diff --git a/test/common.dart b/test/common.dart index cb9ffe1e..97f7c0b1 100644 --- a/test/common.dart +++ b/test/common.dart @@ -1,6 +1,6 @@ library angel_framework.test.common; -import 'package:angel_framework/src/defs.dart'; +import 'package:angel_framework/angel_framework.dart'; class Todo extends MemoryModel { String text; @@ -8,3 +8,16 @@ class Todo extends MemoryModel { Todo({String this.text, String this.over}); } + +incrementTodoTimes(e) { + IncrementService.TIMES++; +} + +@Hooks(before: const [incrementTodoTimes]) +class IncrementService extends Service { + static int TIMES = 0; + + @override + @Hooks(after: const [incrementTodoTimes]) + index([params]) async => []; +} diff --git a/test/hooked_test.dart b/test/hooked_test.dart index 42ea966b..471ff17d 100644 --- a/test/hooked_test.dart +++ b/test/hooked_test.dart @@ -18,12 +18,12 @@ main() { HookedService Todos; setUp(() async { - app = new Angel(debug: true); + app = new Angel(); client = new http.Client(); app.use('/todos', new MemoryService()); Todos = app.service("todos"); - app.dumpTree(showMatchers: true); + // app.dumpTree(showMatchers: true); server = await app.startServer(); url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; @@ -87,4 +87,12 @@ main() { List result = god.deserialize(response.body); expect(result[0]["angel"], equals("framework")); }); + + test('metadata', () async { + final service = new HookedService(new IncrementService())..addHooks(); + expect(service.inner, isNot(new isInstanceOf())); + IncrementService.TIMES = 0; + await service.index(); + expect(IncrementService.TIMES, equals(2)); + }); } From 5312d353f2fe13fc186d9fc60a28b9541975fe28 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 10 Dec 2016 09:12:07 -0500 Subject: [PATCH 063/414] Fixed todo --- .idea/jsLibraryMappings.xml | 7 + .idea/runConfigurations/All_Tests.xml | 7 + .idea/runConfigurations/Controller_Tests.xml | 6 + .idea/runConfigurations/DI_Tests.xml | 6 + .idea/runConfigurations/Hooked_Tests.xml | 6 + .idea/runConfigurations/Routing_Tests.xml | 6 + .idea/workspace.xml | 129 ++++++++++++++----- lib/src/http/fatal_error.dart | 8 ++ lib/src/http/response_context.dart | 1 - lib/src/http/server.dart | 27 ++-- lib/src/http/server_shelved.dart | 19 --- pubspec.yaml | 1 - 12 files changed, 155 insertions(+), 68 deletions(-) create mode 100644 .idea/jsLibraryMappings.xml create mode 100644 .idea/runConfigurations/All_Tests.xml create mode 100644 .idea/runConfigurations/Controller_Tests.xml create mode 100644 .idea/runConfigurations/DI_Tests.xml create mode 100644 .idea/runConfigurations/Hooked_Tests.xml create mode 100644 .idea/runConfigurations/Routing_Tests.xml create mode 100644 lib/src/http/fatal_error.dart delete mode 100644 lib/src/http/server_shelved.dart diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 00000000..f3e502d1 --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml new file mode 100644 index 00000000..ac11209e --- /dev/null +++ b/.idea/runConfigurations/All_Tests.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Controller_Tests.xml b/.idea/runConfigurations/Controller_Tests.xml new file mode 100644 index 00000000..16c24846 --- /dev/null +++ b/.idea/runConfigurations/Controller_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/DI_Tests.xml b/.idea/runConfigurations/DI_Tests.xml new file mode 100644 index 00000000..5c5345d7 --- /dev/null +++ b/.idea/runConfigurations/DI_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Hooked_Tests.xml b/.idea/runConfigurations/Hooked_Tests.xml new file mode 100644 index 00000000..592565a1 --- /dev/null +++ b/.idea/runConfigurations/Hooked_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Routing_Tests.xml b/.idea/runConfigurations/Routing_Tests.xml new file mode 100644 index 00000000..3790ba95 --- /dev/null +++ b/.idea/runConfigurations/Routing_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 9bf5a491..b6251cab 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,17 +1,13 @@ - - - + + + - - - - + + - - @@ -32,26 +28,38 @@ - - + + - - + + - - + + - - + + + + + + + + + + + + + + @@ -121,8 +129,11 @@ @@ -320,7 +331,7 @@ 1481236071442 - + 1481237183504 @@ -329,7 +340,14 @@ -