diff --git a/.idea/runConfigurations/All_Route_Tests.xml b/.idea/runConfigurations/All_Route_Tests.xml index cc751b12..2dda0404 100644 --- a/.idea/runConfigurations/All_Route_Tests.xml +++ b/.idea/runConfigurations/All_Route_Tests.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/All_Router_Tests.xml b/.idea/runConfigurations/All_Router_Tests.xml index a1920311..78458c1b 100644 --- a/.idea/runConfigurations/All_Router_Tests.xml +++ b/.idea/runConfigurations/All_Router_Tests.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/All_Server_Tests.xml b/.idea/runConfigurations/All_Server_Tests.xml index 8a55853b..5388f331 100644 --- a/.idea/runConfigurations/All_Server_Tests.xml +++ b/.idea/runConfigurations/All_Server_Tests.xml @@ -1,6 +1,6 @@ - \ 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/Chain.xml b/.idea/runConfigurations/Chain.xml new file mode 100644 index 00000000..38a60eea --- /dev/null +++ b/.idea/runConfigurations/Chain.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Method_Tests.xml b/.idea/runConfigurations/Method_Tests.xml index 436da978..dd37c59e 100644 --- a/.idea/runConfigurations/Method_Tests.xml +++ b/.idea/runConfigurations/Method_Tests.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/README.md b/README.md index ae6be82f..2bfc485f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_route -![version 1.0.0-dev+8](https://img.shields.io/badge/version-1.0.0--dev+8-red.svg) +![version 1.0.0-dev+9](https://img.shields.io/badge/version-1.0.0--dev+9-red.svg) ![build status](https://travis-ci.org/angel-dart/route.svg) A powerful, isomorphic routing library for Dart. diff --git a/lib/src/route.dart b/lib/src/route.dart index 4f29330c..a11786bf 100644 --- a/lib/src/route.dart +++ b/lib/src/route.dart @@ -177,7 +177,8 @@ class Route { Route result; if (path is RegExp) { - result = new Route(path, debug: debug, handlers: handlers, method: method, name: name); + result = new Route(path, + debug: debug, handlers: handlers, method: method, name: name); } else { final segments = path .toString() @@ -253,7 +254,9 @@ class Route { parent._children.add(route .._matcher = new RegExp('$pattern1$separator$pattern2') - .._head = new RegExp(_matcherify('$path1/$path2'.replaceAll(_straySlashes, '')).replaceAll(_rgxEnd, '')) + .._head = new RegExp( + _matcherify('$path1/$path2'.replaceAll(_straySlashes, '')) + .replaceAll(_rgxEnd, '')) .._parent = parent .._stub = child.matcher); @@ -300,6 +303,24 @@ class Route { return addChild(route); } + Route clone() { + final Route route = new Route(''); + + return route + .._children.addAll(children) + .._handlers.addAll(handlers) + .._head = _head + .._matcher = _matcher + .._method = _method + .._name = name + .._parent = _parent + .._parentResolver = _parentResolver + .._pathified = _pathified + .._resolver = _resolver + .._stub = _stub + ..state.properties.addAll(state.properties); + } + /// Generates a URI to this route with the given parameters. String makeUri([Map params]) { String result = _pathify(path); @@ -324,12 +345,13 @@ class Route { Iterable values = _parseParameters(requestPath.replaceAll(_straySlashes, '')); - _printDebug('Searched request path $requestPath and found these values: $values'); + _printDebug( + 'Searched request path $requestPath and found these values: $values'); final pathString = _pathify(path).replaceAll(new RegExp('\/'), r'\/'); - Iterable matches = - _param.allMatches(pathString); - _printDebug('All param names parsed in $pathString: ${matches.map((m) => m.group(0))}'); + Iterable matches = _param.allMatches(pathString); + _printDebug( + 'All param names parsed in $pathString: ${matches.map((m) => m.group(0))}'); for (int i = 0; i < matches.length && i < values.length; i++) { Match match = matches.elementAt(i); diff --git a/lib/src/router.dart b/lib/src/router.dart index a7164782..130e685b 100644 --- a/lib/src/router.dart +++ b/lib/src/router.dart @@ -113,6 +113,24 @@ class Router extends Extensible { } */ } + /// Returns a [Router] with a duplicated version of this tree. + Router clone({bool normalize: true}) { + final router = new Router(debug: debug); + + _copy(Route route, Route parent) { + final r = route.clone(); + parent._children.add(r.._parent = parent); + + route.children.forEach((child) => _copy(child, r)); + } + + root.children.forEach((child) => _copy(child, router.root)); + + if (normalize) router.normalize(); + + return router; + } + /// Creates a visual representation of the route hierarchy and /// passes it to a callback. If none is provided, `print` is called. void dumpTree( @@ -151,8 +169,7 @@ class Router extends Extensible { } tabs++; - route.children - .forEach((r) => dumpRoute(r, replace: route.path)); + route.children.forEach((r) => dumpRoute(r, replace: route.path)); tabs--; } @@ -210,6 +227,30 @@ class Router extends Extensible { return _resolve(root, _path, method, segments.first, segments.skip(1)); } + /// Finds every possible [Route] that matches the given path, + /// with the given method. + /// + /// This is preferable to [resolve]. + /// Keep in mind that this function uses either a [linearClone] or a [clone], and thus + /// will not return the same exact routes from the original tree. + Iterable resolveAll(String path, + {bool linear: true, String method: 'GET', bool normalizeClone: true}) { + final router = linear + ? linearClone(normalize: normalizeClone) + : clone(normalize: normalizeClone); + final routes = []; + var resolved = router.resolve(path, method: method); + + while (resolved != null) { + routes.add(resolved); + router.root._children.remove(resolved); + + resolved = router.resolve(path, method: method); + } + + return routes.where((route) => route != null); + } + _validHead(RegExp rgx) { return !rgx.hasMatch(''); } @@ -298,10 +339,16 @@ class Router extends Extensible { } } - /// Flattens the route tree into a linear list. + /// Flattens the route tree into a linear list, in-place. void flatten() { + _root = linearClone().root; + } + + /// Returns a [Router] with a linear version of this tree. + Router linearClone({bool normalize: true}) { final router = new Router(debug: debug); - normalize(); + + if (normalize) this.normalize(); _flatten(Route parent, Route route) { // if (route.children.isNotEmpty && route.method == '*') return; @@ -315,8 +362,7 @@ class Router extends Extensible { .._method = route.method .._name = route.name .._parent = route.parent // router.root - .._path = route - .path; //'${parent.path}/${route.path}'.replaceAll(_straySlashes, ''); + .._path = route.path; // New matcher final part1 = parent.matcher.pattern @@ -340,7 +386,7 @@ class Router extends Extensible { } root._children.forEach((child) => _flatten(root, child)); - _root = router.root; + return router; } /// Incorporates another [Router]'s routes into this one's. @@ -431,10 +477,11 @@ class Router extends Extensible { if (merge) { _printDebug('Erasing this route: $route'); - route.parent._handlers.addAll(route.handlers); + // route.parent._handlers.addAll(route.handlers); for (Route child in route.children) { route.parent._children.insert(index, child.._parent = route.parent); + child._handlers.insertAll(0, route.handlers); } route.parent._children.remove(route); diff --git a/test/all_test.dart b/test/all_test.dart deleted file mode 100644 index 3a09d0a9..00000000 --- a/test/all_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:test/test.dart'; -import 'method/all_tests.dart' as method; -import 'route/all_tests.dart' as route; -import 'router/all_tests.dart' as router; -import 'server/all_tests.dart' as server; - -main() { - group('method', method.main); - group('route', route.main); - group('router', router.main); - group('server', server.main); -} \ No newline at end of file diff --git a/test/all_tests.browser.dart b/test/all_tests.browser.dart index e4514b2d..fab39268 100644 --- a/test/all_tests.browser.dart +++ b/test/all_tests.browser.dart @@ -1,6 +1,6 @@ import 'package:test/test.dart'; -import 'route/all_tests.dart' as route; -import 'router/all_tests.dart' as router; +import 'route/all_test.dart' as route; +import 'router/all_test.dart' as router; main() { group('route', route.main); diff --git a/test/chain/all_test.dart b/test/chain/all_test.dart new file mode 100644 index 00000000..f5161fa7 --- /dev/null +++ b/test/chain/all_test.dart @@ -0,0 +1,177 @@ +import 'dart:io'; +import 'package:angel_route/angel_route.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +main() { + http.Client client; + Router router; + HttpServer server; + String url; + + setUp(() async { + client = new http.Client(); + router = new Router(); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); + url = 'http://${server.address.address}:${server.port}'; + + router.get('/hello', (req) { + req.response.write('world'); + }); + + router.get('/sandwich', (req) { + req.response.write('pb'); + return true; + }); + + router.all('/sandwich', (req) { + req.response.write('&j'); + return false; + }); + + router.all('/chain', (req) { + req.response.write('PassTo'); + return true; + }); + + router.group('/group/:id', (router) { + router.get('/fun', (req) { + req.response.write(' and fun!'); + return false; + }, middleware: [ + (req) { + req.response.write(' is cool'); + return true; + } + ]); + }, middleware: [ + (req) { + req.response.write('Dart'); + return true; + } + ]); + + final beatles = new Router(); + + beatles.get('/come-together', (req) { + req.response.write('spinal'); + return true; + }); + + beatles.all('*', (req) { + req.response.write('-clacker'); + return !req.uri.toString().contains('come-together'); + }); + + router.mount('/beatles', beatles); + + router.all('*', (req) { + req.response.write('Fallback'); + return false; + }); + + router + ..normalize() + ..dumpTree(showMatchers: true); + + server.listen((request) async { + final resolved = + router.resolveAll(request.uri.path, method: request.method); + + if (resolved.isEmpty) { + request.response.statusCode = 404; + request.response.write('404 Not Found'); + await request.response.close(); + } else { + print('Resolved ${request.uri} => $resolved'); + + // Easy middleware pipeline + final pipeline = []; + + for (Route route in resolved) { + pipeline.addAll(route.handlerSequence); + } + + print('Pipeline: ${pipeline.length} handler(s)'); + + for (final handler in pipeline) { + if (handler(request) != true) break; + } + + await request.response.close(); + } + }); + }); + + tearDown(() async { + client.close(); + client = null; + router = null; + url = null; + await server.close(); + }); + + test('hello', () async { + final response = await client.get('$url/hello'); + print('Response: ${response.body}'); + expect(response.body, equals('world')); + }); + + test('sandwich', () async { + final response = await client.get('$url/sandwich'); + print('Response: ${response.body}'); + expect(response.body, equals('pb&j')); + }); + + test('chain', () async { + final response = await client.get('$url/chain'); + print('Response: ${response.body}'); + expect(response.body, equals('PassToFallback')); + }); + + test('fallback', () async { + final response = await client.get('$url/fallback'); + print('Response: ${response.body}'); + expect(response.body, equals('Fallback')); + }); + + group('group', () { + test('fun', () async { + final response = await client.get('$url/group/abc/fun'); + print('Response: ${response.body}'); + expect(response.body, equals('Dart is cool and fun!')); + }); + + test('fallback', () async { + final response = await client.get('$url/group/abc'); + print('Response: ${response.body}'); + expect(response.body, equals('Fallback')); + }); + }); + + group('beatles', () { + test('spinal clacker', () async { + final response = await client.get('$url/beatles/come-together'); + print('Response: ${response.body}'); + expect(response.body, equals('spinal-clacker')); + }); + + group('fallback', () { + setUp(() { + router.linearClone().dumpTree(header: 'LINEAR', showMatchers: true); + }); + + test('non-existent', () async { + var response = await client.get('$url/beatles/ringo-starr'); + print('Response: ${response.body}'); + expect(response.body, equals('-clackerFallback')); + }); + + test('root', () async { + var response = await client.get('$url/beatles'); + print('Response: ${response.body}'); + expect(response.body, equals('Fallback')); + }); + }); + }); +} diff --git a/test/method/all_tests.dart b/test/method/all_test.dart similarity index 100% rename from test/method/all_tests.dart rename to test/method/all_test.dart diff --git a/test/route/all_tests.dart b/test/route/all_test.dart similarity index 100% rename from test/route/all_tests.dart rename to test/route/all_test.dart diff --git a/test/router/all_tests.dart b/test/router/all_test.dart similarity index 100% rename from test/router/all_tests.dart rename to test/router/all_test.dart diff --git a/test/server/all_tests.dart b/test/server/all_test.dart similarity index 90% rename from test/server/all_tests.dart rename to test/server/all_test.dart index 34e16dd6..7758e791 100644 --- a/test/server/all_tests.dart +++ b/test/server/all_test.dart @@ -22,11 +22,7 @@ main() { url = 'http://${server.address.address}:${server.port}'; server.listen((request) async { - final resolved = router.resolveOnRoot(request.uri.toString(), filter: (route) { - print( - '$route matches ${request.method} ${request.uri}? ${route.method == request.method || route.method == '*'}'); - return route.method == request.method || route.method == '*'; - }); + final resolved = router.resolve(request.uri.path, method: request.method); if (resolved == null) { request.response.statusCode = 404;