From 1b37e0a2a37322ccdb40bb05dd97ec986c81f40c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 20 Oct 2016 23:13:13 -0400 Subject: [PATCH] All tests now passing --- .idea/angel_route.iml | 1 + .idea/runConfigurations/All_Server_Tests.xml | 6 + lib/browser.dart | 4 +- lib/src/route.dart | 168 ++++++++++++------- lib/src/router.dart | 46 ++--- pubspec.yaml | 1 + test/all_tests.browser.dart | 8 + test/all_tests.dart | 2 + test/index.html | 2 +- test/route/with_params.dart | 4 +- test/router/all_tests.dart | 7 +- test/router/use.dart | 26 ++- test/server/all_tests.dart | 114 +++++++++++++ 13 files changed, 294 insertions(+), 95 deletions(-) create mode 100644 .idea/runConfigurations/All_Server_Tests.xml create mode 100644 test/all_tests.browser.dart create mode 100644 test/server/all_tests.dart diff --git a/.idea/angel_route.iml b/.idea/angel_route.iml index 1aa3806d..b4a2e37b 100644 --- a/.idea/angel_route.iml +++ b/.idea/angel_route.iml @@ -9,6 +9,7 @@ + diff --git a/.idea/runConfigurations/All_Server_Tests.xml b/.idea/runConfigurations/All_Server_Tests.xml new file mode 100644 index 00000000..8a55853b --- /dev/null +++ b/.idea/runConfigurations/All_Server_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/lib/browser.dart b/lib/browser.dart index fe66108d..9bf55fa9 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -17,7 +17,7 @@ abstract class BrowserRouter extends Router { : new _PushStateRouter(listen: listen, root: root); } - BrowserRouter._([Route root]) : super(root); + BrowserRouter._([Route root]) : super(root: root); /// Calls `goTo` on the [Route] matching `path`. void go(String path, [Map params]); @@ -37,7 +37,7 @@ class _BrowserRouterImpl extends Router implements BrowserRouter { @override Stream get onRoute => _onRoute.stream; - _BrowserRouterImpl({bool listen, Route root}) : super(root) { + _BrowserRouterImpl({bool listen, Route root}) : super(root: root) { if (listen) this.listen(); prepareAnchors(); } diff --git a/lib/src/route.dart b/lib/src/route.dart index e711a365..3b620d57 100644 --- a/lib/src/route.dart +++ b/lib/src/route.dart @@ -115,6 +115,15 @@ class Route { return result; } + /// Returns the [Route] instance that will respond to requests + /// to the index of this instance's path. + /// + /// May return `this`. + Route get indexRoute { + return children.firstWhere((r) => r.path.replaceAll(path, '').isEmpty, + orElse: () => this); + } + void _printDebug(msg) { if (debug) print(msg); } @@ -157,6 +166,7 @@ class Route { /// The final child route is returned. factory Route.build(Pattern path, {Iterable children: const [], + bool debug: false, Iterable handlers: const [], method: "GET", String name: null}) { @@ -169,7 +179,11 @@ class Route { if (segments.isEmpty) { return new Route('/', - children: children, handlers: handlers, method: method, name: name); + children: children, + debug: debug, + handlers: handlers, + method: method, + name: name); } for (int i = 0; i < segments.length; i++) { @@ -177,15 +191,15 @@ class Route { if (i == segments.length - 1) { if (result == null) { - result = new Route(segment); + result = new Route(segment, debug: debug); } else { - result = result.child(segment); + result = result.child(segment, debug: debug); } } else { if (result == null) { - result = new Route(segment, method: "*"); + result = new Route(segment, debug: debug, method: "*"); } else { - result = result.child(segment, method: "*"); + result = result.child(segment, debug: debug, method: "*"); } } } @@ -195,11 +209,11 @@ class Route { result._method = method; result._name = name; - return result; + return result..debug = debug; } /// Combines the paths and matchers of two [Route] instances, and creates a new instance. - factory Route.join(Route parent, Route child) { + factory Route.join(Route parent, Route child, {bool debug: false}) { final String path1 = parent.path .replaceAll(_rgxStart, '') .replaceAll(_rgxEnd, '') @@ -217,7 +231,6 @@ class Route { final route = new Route('$path1/$path2', children: child.children, - debug: parent.debug || child.debug, handlers: child.handlers, method: child.method, name: child.name); @@ -228,10 +241,29 @@ class Route { .._matcher = new RegExp('$pattern1$separator$pattern2') .._parent = parent .._stub = child.matcher); - parent._printDebug( - 'Joined $path1 and $path2, produced stub ${route._stub.pattern}'); - return route; + parent._printDebug( + "Joined '/$path1' and '/$path2', created stub: ${route._stub.pattern}"); + + return route..debug = parent.debug || child.debug || debug; + } + + Route _inherit(Route route) { + /* + final List _children = []; + final List _handlers = []; + RegExp _matcher; + String _method; + String _name; + Route _parent; + RegExp _parentResolver; + String _path; + String _pathified; + RegExp _resolver; + RegExp _stub; + + */ + return route.._parent = this; } /// Calls [addChild] on all given routes. @@ -241,7 +273,14 @@ class Route { /// Adds the given route as a hierarchical child of this one. Route addChild(Route route, {bool join: true}) { - Route created = join ? new Route.join(this, route) : route.._parent = this; + Route created; + + if (join) { + created = new Route.join(this, route); + } else { + _children.add(created = route.._parent = this); + } + return created..debug = debug; } @@ -251,11 +290,16 @@ class Route { /// Creates a hierarchical child of this route with the given path. Route child(Pattern path, {Iterable children: const [], + bool debug: false, Iterable handlers: const [], String method: "GET", String name: null}) { final route = new Route.build(path, - children: children, handlers: handlers, method: method, name: name); + children: children, + debug: debug, + handlers: handlers, + method: method, + name: name); return addChild(route); } @@ -310,25 +354,39 @@ class Route { /// /// Can be used to navigate a route hierarchy like a file system. Route resolve(String path, {bool filter(Route route), String fullPath}) { - final _filter = filter ?? (_) => true; + bool _filter(route) { + if (filter == null) { + _printDebug('No filter provided, returning true for $route'); + return true; + } else { + _printDebug('Running filter on $route'); + final result = filter(route); + _printDebug('Filter result: $result'); + return result; + } + } + final _fullPath = fullPath ?? path; - if ((path.isEmpty || path == '.') && _filter(this)) { + if ((path.isEmpty || path == '.') && _filter(indexRoute)) { // Try to find index - _printDebug('INDEX???'); - return children.firstWhere((r) => r.path.isEmpty, orElse: () => this); + _printDebug('Empty path, resolving with indexRoute: $indexRoute'); + return indexRoute; } else if (path == '/') { return absoluteParent.resolve(''); } else if (path.replaceAll(_straySlashes, '').isEmpty) { for (Route route in children) { final stub = route.path.replaceAll(this.path, ''); - if ((stub == '/' || stub.isEmpty) && _filter(route)) return route; + if ((stub == '/' || stub.isEmpty) && _filter(route)) + return route.resolve(''); } - if (_filter(this)) - return this; - else + if (_filter(indexRoute)) { + _printDebug( + 'Path "/$path" is technically empty, sending to indexRoute: $indexRoute'); + return indexRoute; + } else return null; } else if (path == '..') { if (parent != null) @@ -343,13 +401,16 @@ class Route { filter: _filter, fullPath: _fullPath); } else if (matcher.hasMatch(path.replaceAll(_straySlashes, '')) || _resolver.hasMatch(path.replaceAll(_straySlashes, ''))) { - return this; + _printDebug( + 'Path "/$path" matched our matcher, sending to indexRoute: $indexRoute'); + return indexRoute; } else { final segments = path.split('/').where((str) => str.isNotEmpty).toList(); _printDebug('Segments: $segments on "/${this.path}"'); if (segments.isEmpty) { - return children.firstWhere((r) => r.path.isEmpty, orElse: () => this); + _printDebug('Empty segments, sending to indexRoute: $indexRoute'); + return indexRoute; } if (segments[0] == '..') { @@ -366,12 +427,12 @@ class Route { for (Route route in children) { final subPath = '${this.path}/${segments[0]}'; _printDebug( - 'seg0: ${segments[0]}, stub: ${route._stub.pattern}, path: $path, route.path: ${route.path}, route.matcher: ${route.matcher.pattern}, this.matcher: ${matcher.pattern}'); + 'seg0: ${segments[0]}, stub: ${route._stub?.pattern}, path: $path, route.path: ${route.path}, route.matcher: ${route.matcher.pattern}, this.matcher: ${matcher.pattern}'); if (route.match(subPath) != null || route._resolver.firstMatch(subPath) != null) { if (segments.length == 1 && _filter(route)) - return route; + return route.resolve(''); else { return route.resolve(segments.skip(1).join('/'), filter: _filter, @@ -380,8 +441,14 @@ class Route { _fullPath.replaceAll(_straySlashes, '')); } } else if (route._stub != null && route._stub.hasMatch(segments[0])) { - _printDebug('MAYBE STUB?'); - return route; + if (segments.length == 1) { + _printDebug('Stub perhaps matches'); + return route.resolve(''); + } else { + _printDebug( + 'Maybe stub matches. Sending remaining segments to $route'); + return route.resolve(segments.skip(1).join('/')); + } } } @@ -405,38 +472,11 @@ class Route { (child.match(_fullPath) != null || child._resolver.firstMatch(_fullPath) != null) && _filter(child)) { - return child; + return child.resolve(''); } } - - _printDebug('No subpath match: $subPath'); - } else - _printDebug('Nope: $_parentResolver'); - } - - /* - // Try to fill params - for (Route route in children) { - final params = parseParameters(_fullPath); - final _filledPath = makeUri(params); - _printDebug( - 'Trying to match filled $_filledPath for ${route.path} on ${this.path}'); - if ((route.match(_filledPath) != null || - route._resolver.firstMatch(_filledPath) != null) && - _filter(route)) - return route; - else if ((route.match(_filledPath) != null || - route._resolver.firstMatch(_filledPath) != null) && - _filter(route)) - return route; - else if ((route.match('/$_filledPath') != null || - route._resolver.firstMatch('/$_filledPath') != null) && - _filter(route)) - return route; - else { - _printDebug('Failed for ${route.matcher} when given $_filledPath'); } - }*/ + } // Try to match the whole route, if nothing else works for (Route route in children) { @@ -445,18 +485,20 @@ class Route { if ((route.match(_fullPath) != null || route._resolver.firstMatch(_fullPath) != null) && _filter(route)) - return route; + return route.resolve(''); else if ((route.match(_fullPath) != null || route._resolver.firstMatch(_fullPath) != null) && _filter(route)) - return route; + return route.resolve(''); else if ((route.match('/$_fullPath') != null || route._resolver.firstMatch('/$_fullPath') != null) && - _filter(route)) - return route; - else { - _printDebug('Failed for ${route.matcher} when given $_fullPath'); - } + _filter(route)) return route.resolve(''); + } + + // Lastly, check to see if we have an index route to resolve with + if (indexRoute != this) { + _printDebug('Forwarding "/$path" to indexRoute'); + return indexRoute.resolve(path); } return null; diff --git a/lib/src/router.dart b/lib/src/router.dart index e35588df..025e9d62 100644 --- a/lib/src/router.dart +++ b/lib/src/router.dart @@ -6,6 +6,8 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); /// An abstraction over complex [Route] trees. Use this instead of the raw API. :) class Router extends Extensible { + Route _root; + /// Set to `true` to print verbose debug output when interacting with this route. bool debug = false; @@ -13,15 +15,16 @@ class Router extends Extensible { Map requestMiddleware = {}; /// The single [Route] that serves as the root of the hierarchy. - final Route root; + Route get root => _root; /// Provide a `root` to make this Router revolve around a pre-defined route. /// Not recommended. - Router([Route root]) : this.root = root ?? new _RootRoute(); + Router({this.debug: false, Route root}) { + _root = (_root = root ?? new _RootRoute())..debug = debug; + } void _printDebug(msg) { - if (debug) - _printDebug(msg); + if (debug) print(msg); } /// Adds a route that responds to the given path @@ -36,9 +39,10 @@ class Router extends Extensible { ..add(handler); if (path is RegExp) { - return root.child(path, handlers: handlers, method: method); + return root.child(path, debug: debug, handlers: handlers, method: method); } else if (path.toString().replaceAll(_straySlashes, '').isEmpty) { - return root.child(path.toString(), handlers: handlers, method: method); + return root.child(path.toString(), + debug: debug, handlers: handlers, method: method); } else { var segments = path .toString() @@ -48,9 +52,9 @@ class Router extends Extensible { Route result; if (segments.isEmpty) { - return new Route('/', handlers: handlers, method: method); + return new Route('/', debug: debug, handlers: handlers, method: method) + ..debug = debug; } else { - _printDebug('Want ${segments[0]}'); result = resolve(segments[0]); if (result != null) { @@ -67,7 +71,9 @@ class Router extends Extensible { result = existing; } } while (existing != null); - } else throw new RoutingException("Cannot overwrite existing route '${segments[0]}'."); + } else + throw new RoutingException( + "Cannot overwrite existing route '${segments[0]}'."); } } @@ -76,15 +82,17 @@ class Router extends Extensible { if (i == segments.length - 1) { if (result == null) { - result = root.child(segment, handlers: handlers, method: method); + result = root.child(segment, + debug: debug, handlers: handlers, method: method); } else { - result = result.child(segment, handlers: handlers, method: method); + result = result.child(segment, + debug: debug, handlers: handlers, method: method); } } else { if (result == null) { - result = root.child(segment, method: "*"); + result = root.child(segment, debug: debug, method: "*"); } else { - result = result.child(segment, method: "*"); + result = result.child(segment, debug: debug, method: "*"); } } } @@ -105,7 +113,8 @@ class Router extends Extensible { if (route == root) buf.write('(root) ${route.method} '); - else buf.write('- ${route.method} '); + else + buf.write('- ${route.method} '); final p = replace != null ? route.path.replaceAll(replace, '') : route.path; @@ -143,7 +152,7 @@ class Router extends Extensible { String namespace: null}) { final route = root.child(path, handlers: middleware, method: method, name: name); - final router = new Router(route); + final router = new Router(root: route); callback(router); // Let's copy middleware, heeding the optional middleware namespace. @@ -191,7 +200,7 @@ class Router extends Extensible { copiedMiddleware[middlewareName]; } - root.child(path).addChild(router.root); + root.child(path, debug: debug).addChild(router.root); } /// Adds a route that responds to any request matching the given path. @@ -236,9 +245,8 @@ class Router extends Extensible { } class _RootRoute extends Route { - _RootRoute():super("/", name: ""); - + _RootRoute() : super("/", name: ""); @override String toString() => "ROOT"; -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 855e6c87..ce4c4909 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,4 +5,5 @@ author: Tobe O homepage: https://github.com/angel-dart/angel_route dev_dependencies: browser: ">=0.10.0 < 0.11.0" + http: ">=0.11.3 <0.12.0" test: ">=0.12.15 <0.13.0" \ No newline at end of file diff --git a/test/all_tests.browser.dart b/test/all_tests.browser.dart new file mode 100644 index 00000000..e4514b2d --- /dev/null +++ b/test/all_tests.browser.dart @@ -0,0 +1,8 @@ +import 'package:test/test.dart'; +import 'route/all_tests.dart' as route; +import 'router/all_tests.dart' as router; + +main() { + group('route', route.main); + group('router', router.main); +} \ No newline at end of file diff --git a/test/all_tests.dart b/test/all_tests.dart index e4514b2d..b8706fc8 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -1,8 +1,10 @@ import 'package:test/test.dart'; import 'route/all_tests.dart' as route; import 'router/all_tests.dart' as router; +import 'server/all_tests.dart' as server; main() { group('route', route.main); group('router', router.main); + group('server', server.main); } \ No newline at end of file diff --git a/test/index.html b/test/index.html index d1cbd0f0..24f2d4bc 100644 --- a/test/index.html +++ b/test/index.html @@ -5,7 +5,7 @@ Tests - + \ No newline at end of file diff --git a/test/route/with_params.dart b/test/route/with_params.dart index de0e06bc..e983a1e0 100644 --- a/test/route/with_params.dart +++ b/test/route/with_params.dart @@ -7,7 +7,7 @@ main() { final bar = fooById.child('bar'); final baz = bar.child('//////baz//////', handlers: ['hello', 'world']); final bazById = baz.child(':bazId([A-Za-z]+)'); - new Router(foo).dumpTree(); + new Router(root: foo).dumpTree(); test('matching', () { expect(fooById.children.length, equals(1)); @@ -54,7 +54,7 @@ main() { expect(bar.resolve('..'), equals(fooById)); - new Router(bar.parent).dumpTree(header: "POOP"); + new Router(root: bar.parent).dumpTree(header: "POOP"); expect(bar.parent.resolve('bar/baz'), equals(baz)); expect(bar.resolve('/2/bar/baz'), equals(baz)); expect(bar.resolve('../bar'), equals(bar)); diff --git a/test/router/all_tests.dart b/test/router/all_tests.dart index 655da558..5c83e24a 100644 --- a/test/router/all_tests.dart +++ b/test/router/all_tests.dart @@ -9,7 +9,8 @@ main() { final router = new Router(); final indexRoute = router.get('/', () => ':)'); final fizz = router.post('/user/fizz', null); - final deleteUserById = router.delete('/user/:id/detail', (id) => num.parse(id)); + final deleteUserById = + router.delete('/user/:id/detail', (id) => num.parse(id)); Route lower; final letters = router.group('/letter///', (router) { @@ -29,6 +30,7 @@ main() { }); group('fallback', fallback.main); + test('group & use', use.main); test('hierarchy', () { expect(lower.absoluteParent, equals(router.root)); @@ -45,7 +47,4 @@ main() { expect(router.resolve('letter/a/lower'), equals(lower)); expect(router.resolve('letter/2/lower'), isNull); }); - - - test('use', use.main); } diff --git a/test/router/use.dart b/test/router/use.dart index 59496fcb..d7fcf628 100644 --- a/test/router/use.dart +++ b/test/router/use.dart @@ -1,17 +1,35 @@ import 'package:angel_route/angel_route.dart'; import 'package:test/test.dart'; +final String ARTIFICIAL_INDEX = 'artificial index'; + +tattle(x) => 'This ${x.runtimeType}.debug = ${x.debug}'; +tattleAll(x) => x.map(tattle).join('\n'); + main() { - final parent = new Router()..debug = true; - final child = new Router()..debug = true; - final a = child.get('a', ['c']); + final parent = new Router(debug: true); + final child = new Router(debug: true); + Route a, b, c; + + a = child.get('a', ['c']); + child.group('b', (router) { + b = router.get('/', ARTIFICIAL_INDEX); + c = router.post('c', 'Hello nested'); + }); + parent.use('child', child); - parent.dumpTree(); + parent.dumpTree(header: tattleAll([parent, child, a])); group('no params', () { test('resolve', () { + expect(child.resolve('a'), equals(a)); + expect(child.resolve('b'), equals(b)); + expect(child.resolve('b/c'), equals(c)); + expect(parent.resolve('child/a'), equals(a)); expect(parent.resolve('a'), isNull); + expect(parent.resolve('child/b'), equals(b)); + expect(parent.resolve('child/b/c'), equals(c)); }); }); } diff --git a/test/server/all_tests.dart b/test/server/all_tests.dart new file mode 100644 index 00000000..8469fa8d --- /dev/null +++ b/test/server/all_tests.dart @@ -0,0 +1,114 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:io'; +import 'package:angel_route/angel_route.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +typedef Future RequestHandler(HttpRequest request); + +final String MIDDLEWARE_GREETING = 'Hi, I am a middleware!'; + +main() { + http.Client client; + Router router; + HttpServer server; + String url; + + setUp(() async { + client = new http.Client(); + router = new Router(debug: true); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); + url = 'http://${server.address.address}:${server.port}'; + + server.listen((request) async { + final resolved = router.resolve(request.uri.toString(), (route) { + print( + '$route matches ${request.method} ${request.uri}? ${route.method == request.method || route.method == '*'}'); + return route.method == request.method || route.method == '*'; + }); + + if (resolved == null) { + request.response.statusCode = 404; + request.response.write('404 Not Found'); + await request.response.close(); + } else { + // Easy middleware pipeline + for (final handler in resolved.handlerSequence) { + if (handler is String) { + if (!await router.requestMiddleware[handler](request)) break; + } else if (!await handler(request)) { + break; + } + } + + await request.response.close(); + } + }); + + router.get('foo', (HttpRequest request) async { + request.response.write('bar'); + return false; + }); + + Route square; + + square = router.post('square/:num([0-9]+)', (HttpRequest request) async { + final params = square.parseParameters(request.uri.toString()); + final squared = math.pow(params['num'], 2); + request.response.statusCode = squared; + request.response.write(squared); + return false; + }); + + router.group('todos', (router) { + router.get('/', (HttpRequest request) async { + print('TODO INDEX???'); + request.response.write([]); + return false; + }); + }, middleware: [ + (HttpRequest request) async { + request.response.write(MIDDLEWARE_GREETING); + return true; + } + ]); + + router.dumpTree(); + }); + + tearDown(() async { + client.close(); + client = null; + router = null; + url = null; + await server.close(); + }); + + group('group', () { + test('todo index', () async { + final response = await client.get('$url/todos'); + expect(response.statusCode, equals(200)); + expect(response.body, equals('$MIDDLEWARE_GREETING[]')); + }); + }); + + group('top-level route', () { + test('no params', () async { + final response = await client.get('$url/foo'); + expect(response.statusCode, equals(200)); + expect(response.body, equals('bar')); + }); + + test('with params', () async { + final response = await client.post('$url/square/16'); + expect(response.statusCode, equals(256)); + expect(response.body, equals(response.statusCode.toString())); + }); + + test('throw 404', () async { + final response = await client.get('$url/abc'); + expect(response.statusCode, equals(404)); + }); + }); +}