diff --git a/.idea/runConfigurations/All_Route_Tests.xml b/.idea/runConfigurations/All_Route_Tests.xml deleted file mode 100644 index 2dda0404..00000000 --- a/.idea/runConfigurations/All_Route_Tests.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/All_Router_Tests.xml b/.idea/runConfigurations/All_Router_Tests.xml deleted file mode 100644 index 78458c1b..00000000 --- a/.idea/runConfigurations/All_Router_Tests.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/All_Server_Tests.xml b/.idea/runConfigurations/All_Server_Tests.xml deleted file mode 100644 index 5388f331..00000000 --- a/.idea/runConfigurations/All_Server_Tests.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Chain.xml b/.idea/runConfigurations/Chain.xml deleted file mode 100644 index 38a60eea..00000000 --- a/.idea/runConfigurations/Chain.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Fallback.xml b/.idea/runConfigurations/Fallback.xml deleted file mode 100644 index 4e88d8db..00000000 --- a/.idea/runConfigurations/Fallback.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Method_Tests.xml b/.idea/runConfigurations/Method_Tests.xml deleted file mode 100644 index dd37c59e..00000000 --- a/.idea/runConfigurations/Method_Tests.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Navigate_Tests.xml b/.idea/runConfigurations/Navigate_Tests.xml new file mode 100644 index 00000000..40a1098f --- /dev/null +++ b/.idea/runConfigurations/Navigate_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/No_Params.xml b/.idea/runConfigurations/No_Params.xml deleted file mode 100644 index b1e8cfdf..00000000 --- a/.idea/runConfigurations/No_Params.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Parse_Params.xml b/.idea/runConfigurations/Parse_Params.xml deleted file mode 100644 index b5ef0380..00000000 --- a/.idea/runConfigurations/Parse_Params.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Server_Tests.xml b/.idea/runConfigurations/Server_Tests.xml new file mode 100644 index 00000000..d2df89e9 --- /dev/null +++ b/.idea/runConfigurations/Server_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Use.xml b/.idea/runConfigurations/Use.xml deleted file mode 100644 index 4ba81c55..00000000 --- a/.idea/runConfigurations/Use.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/With_Params.xml b/.idea/runConfigurations/With_Params.xml deleted file mode 100644 index 1d95c2c7..00000000 --- a/.idea/runConfigurations/With_Params.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/lib/angel_route.dart b/lib/angel_route.dart index bcbee5dd..0f0a79d1 100644 --- a/lib/angel_route.dart +++ b/lib/angel_route.dart @@ -1,5 +1,6 @@ -/// A powerful, isomorphic routing library for Dart. library angel_route; +export 'src/extensible.dart'; +export 'src/middleware_pipeline.dart'; export 'src/router.dart'; export 'src/routing_exception.dart'; \ No newline at end of file diff --git a/lib/browser.dart b/lib/browser.dart deleted file mode 100644 index 39220f21..00000000 --- a/lib/browser.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'dart:async' show Stream, StreamController; -import 'dart:html' show AnchorElement, window; -import 'angel_route.dart'; - -final RegExp _hash = new RegExp(r'^#/'); - -/// A variation of the [Router] support both hash routing and push state. -abstract class BrowserRouter extends Router { - /// Fires whenever the active route changes. Fires `null` if none is selected (404). - Stream get onRoute; - - /// Set `hash` to true to use hash routing instead of push state. - /// `listen` as `true` will call `listen` after initialization. - factory BrowserRouter({bool hash: false, bool listen: true, Route root}) { - return hash - ? new _HashRouter(listen: listen, root: root) - : new _PushStateRouter(listen: listen, root: root); - } - - BrowserRouter._([Route root]) : super(root: root); - - /// Calls `goTo` on the [Route] matching `path`. - void go(String path, [Map params]); - - /// Navigates to the given route. - void goTo(Route route, [Map params]); - - /// Begins listen for location changes. - void listen(); -} - -class _BrowserRouterImpl extends Router implements BrowserRouter { - Route _current; - StreamController _onRoute = new StreamController(); - Route get currentRoute => _current; - - @override - Stream get onRoute => _onRoute.stream; - - _BrowserRouterImpl({bool listen, Route root}) : super(root: root) { - if (listen) this.listen(); - prepareAnchors(); - } - - @override - void go(String path, [Map params]) { - final resolved = resolve(path); - - if (resolved != null) - goTo(resolved, params); - else - throw new RoutingException.noSuchRoute(path); - } - - @override - void listen() { - normalize(); - } - - void prepareAnchors() { - final anchors = window.document.querySelectorAll('a:not([dynamic])'); - - for (final AnchorElement $a in anchors) { - if ($a.attributes.containsKey('href') && - !$a.attributes.containsKey('download') && - !$a.attributes.containsKey('target') && - $a.attributes['rel'] != 'external') { - $a.onClick.listen((e) { - e.preventDefault(); - go($a.attributes['href']); - }); - } - - $a.attributes['dynamic'] = 'true'; - } - } -} - -class _HashRouter extends _BrowserRouterImpl { - _HashRouter({bool listen, Route root}) : super(listen: listen, root: root) { - if (listen) this.listen(); - } - - @override - void goTo(Route route, [Map params]) { - route.state.properties.addAll(params ?? {}); - window.location.hash = '#/${route.makeUri(params)}'; - _onRoute.add(route); - } - - @override - void listen() { - super.listen(); - window.onHashChange.listen((_) { - final path = window.location.hash.replaceAll(_hash, ''); - final resolved = resolveOnRoot(path); - - if (resolved == null || (path.isEmpty && resolved == root)) { - _onRoute.add(_current = null); - } else if (resolved != null && resolved != _current) { - goTo(resolved); - } - }); - } -} - -class _PushStateRouter extends _BrowserRouterImpl { - _PushStateRouter({bool listen, Route root}) - : super(listen: listen, root: root) { - if (listen) this.listen(); - } - - @override - void goTo(Route route, [Map params]) { - window.history.pushState( - {'path': route.path, 'params': params ?? {}, 'properties': properties}, - route.name ?? route.path, - route.makeUri(params)); - _onRoute.add(_current = route..state.properties.addAll(params ?? {})); - } - - @override - void listen() { - super.listen(); - window.onPopState.listen((e) { - if (e.state is Map && e.state.containsKey('path')) { - final resolved = resolve(e.state['path']); - - if (resolved != _current) { - properties.addAll(e.state['properties'] ?? {}); - _onRoute.add(_current = resolved - ..state.properties.addAll(e.state['params'] ?? {})); - } - } else - _onRoute.add(_current = null); - }); - } -} diff --git a/lib/src/extensible.dart b/lib/src/extensible.dart index 1dd58a26..ebfb89bb 100644 --- a/lib/src/extensible.dart +++ b/lib/src/extensible.dart @@ -28,4 +28,4 @@ class Extensible { super.noSuchMethod(invocation); } -} +} \ No newline at end of file diff --git a/lib/src/middleware_pipeline.dart b/lib/src/middleware_pipeline.dart new file mode 100644 index 00000000..b9a97188 --- /dev/null +++ b/lib/src/middleware_pipeline.dart @@ -0,0 +1,17 @@ +import 'router.dart'; + +class MiddlewarePipeline { + final List routingResults; + + List get handlers { + final handlers = []; + + for (RoutingResult result in routingResults) { + handlers.addAll(result.allHandlers); + } + + return handlers; + } + + MiddlewarePipeline(this.routingResults); +} diff --git a/lib/src/route.dart b/lib/src/route.dart index 56b26b43..ccc5bf3f 100644 --- a/lib/src/route.dart +++ b/lib/src/route.dart @@ -343,7 +343,7 @@ class Route { Map result = {}; Iterable values = - _parseParameters(requestPath.replaceAll(_straySlashes, '')); + _parseParameters(requestPath.replaceAll(_straySlashes, '')); _printDebug( 'Searched request path $requestPath and found these values: $values'); @@ -351,7 +351,7 @@ class Route { 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))}'); + '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); @@ -487,7 +487,7 @@ class Route { if (match != null) { final subPath = - path.replaceFirst(match[0], '').replaceAll(_straySlashes, ''); + path.replaceFirst(match[0], '').replaceAll(_straySlashes, ''); _printDebug("Subdir path: $subPath"); for (Route child in route.children) { @@ -510,12 +510,12 @@ class Route { _printDebug( 'Trying to match full $_fullPath for ${route.path} on ${this.path}'); if ((route.match(_fullPath) != null || - route._resolver.firstMatch(_fullPath) != null) && + route._resolver.firstMatch(_fullPath) != null) && _filter(route)) { _printDebug('Matched full path!'); return route.resolve(''); } else if ((route.match('/$_fullPath') != null || - route._resolver.firstMatch('/$_fullPath') != null) && + route._resolver.firstMatch('/$_fullPath') != null) && _filter(route)) { _printDebug('Matched full path (with a leading slash!)'); return route.resolve(''); @@ -534,4 +534,4 @@ class Route { @override String toString() => "$method '$path' => ${handlers.length} handler(s)"; -} +} \ No newline at end of file diff --git a/lib/src/router.dart b/lib/src/router.dart index e748293b..42e0aa35 100644 --- a/lib/src/router.dart +++ b/lib/src/router.dart @@ -2,8 +2,9 @@ library angel_route.src.router; import 'extensible.dart'; import 'routing_exception.dart'; - +part 'symlink_route.dart'; part 'route.dart'; +part 'routing_result.dart'; final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _rgxEnd = new RegExp(r'\$+$'); @@ -14,23 +15,27 @@ final RegExp _slashDollar = new RegExp(r'/+\$'); 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; +class Router { + final List _middleware = []; + final Map _mounted = {}; + final List _routes = []; /// Set to `true` to print verbose debug output when interacting with this route. bool debug = false; + List get middleware => new List.unmodifiable(_middleware); + + Map get mounted => + new Map.unmodifiable(_mounted); + /// Additional filters to be run on designated requests. Map requestMiddleware = {}; - /// The single [Route] that serves as the root of the hierarchy. - Route get root => _root; + List get routes => new List.unmodifiable(_routes); /// Provide a `root` to make this Router revolve around a pre-defined route. /// Not recommended. - Router({this.debug: false, Route root}) { - _root = (_root = root ?? new _RootRoute())..debug = debug; - } + Router({this.debug: false}); void _printDebug(msg) { if (debug == true) print(msg); @@ -40,142 +45,79 @@ class Router extends Extensible { /// for requests with the given method (case-insensitive). /// Provide '*' as the method to respond to all methods. Route addRoute(String method, Pattern path, Object handler, - {List middleware}) { - List handlers = []; + {List middleware: const []}) { + // Check if any mounted routers can match this + final handlers = [handler]; - handlers - ..addAll(middleware ?? []) - ..add(handler); + if (middleware != null) handlers.addAll(middleware); - if (path is RegExp) { - return root.child(path, debug: debug, handlers: handlers, method: method); - } else { - // if (path.toString().replaceAll(_straySlashes, '').isEmpty || true) { - return root.child(path.toString(), - debug: debug, handlers: handlers, method: method); - } - /* else { - var segments = path - .toString() - .split('/') - .where((str) => str.isNotEmpty) - .toList(growable: false); - Route result; - - if (segments.isEmpty) { - return new Route('/', debug: debug, handlers: handlers, method: method) - ..debug = debug; - } else { - result = resolveOnRoot(segments[0], - filter: (route) => route.method == method || route.method == '*'); - - if (result != null) { - if (segments.length > 1) { - _printDebug('Resolved: ${result} for "${segments[0]}"'); - segments = segments.skip(1).toList(growable: false); - - Route existing; - - do { - existing = result.resolve(segments[0], - filter: (route) => - route.method == method || route.method == '*'); - - if (existing != null) { - result = existing; - } - } while (existing != null); - } - } - } - - for (int i = 0; i < segments.length; i++) { - final segment = segments[i]; - - if (i == segments.length - 1) { - if (result == null) { - result = root.child(segment, - debug: debug, handlers: handlers, method: method); - } else { - result = result.child(segment, - debug: debug, handlers: handlers, method: method); - } - } else { - if (result == null) { - result = root.child(segment, debug: debug, method: "*"); - } else { - result = result.child(segment, debug: debug, method: "*"); - } - } - } - - return result..debug = debug; - } */ + final route = + new Route(path, debug: debug, method: method, handlers: handlers); + _routes.add(route); + return route; } /// Returns a [Router] with a duplicated version of this tree. - Router clone({bool normalize: true}) { + Router clone() { final router = new Router(debug: debug); + final newMounted = new Map.from(mounted); - _copy(Route route, Route parent) { - final r = route.clone(); - parent._children.add(r.._parent = parent); - - route.children.forEach((child) => _copy(child, r)); + for (Route route in routes) { + if (route is! SymlinkRoute) { + router._routes.add(route.clone()); + } else if (route is SymlinkRoute) { + router._routes.add(new SymlinkRoute(route.path, route.pattern, + newMounted[route.pattern] = route.router.clone())); + } } - root.children.forEach((child) => _copy(child, router.root)); - - if (normalize) router.normalize(); - - return router; + return router.._mounted.addAll(newMounted); } /// Creates a visual representation of the route hierarchy and /// passes it to a callback. If none is provided, `print` is called. void dumpTree( {callback(String tree), - header: 'Dumping route tree:', - tab: ' ', - showMatchers: false}) { - var tabs = 0; + String header: 'Dumping route tree:', + String tab: ' ', + bool showMatchers: false}) { final buf = new StringBuffer(); + int tabs = 0; - void dumpRoute(Route route, {Pattern replace: null}) { - for (var i = 0; i < tabs; i++) buf.write(tab); + if (header != null && header.isNotEmpty) { + buf.writeln(header); + } - if (route == root) - buf.writeln('(root)'); - else { - buf.write('- ${route.method} '); + indent() { + for (int i = 0; i < tabs; i++) buf.write(tab); + } - var p = - replace != null ? route.path.replaceAll(replace, '') : route.path; - p = p.replaceAll(_straySlashes, ''); + dumpRouter(Router router) { + indent(); + buf.writeln('- '); + tabs++; - if (p.isEmpty) - buf.write("'/'"); - else - buf.write("'${p.replaceAll(_straySlashes, '')}'"); + for (Route route in router.routes) { + indent(); + buf.write('- ${route.path.isNotEmpty ? route.path : '/'}'); - if (showMatchers) { - buf.write(' (matcher: ${route.matcher.pattern})'); - } - - if (route.handlers.isNotEmpty) - buf.writeln(' => ${route.handlers.length} handler(s)'); - else + if (route is SymlinkRoute) { buf.writeln(); + tabs++; + dumpRouter(route.router); + tabs--; + } else { + if (showMatchers) buf.write(' (${route.matcher.pattern})'); + + buf.writeln(' => ${route.handlers.length} handler(s)'); + } } - tabs++; - route.children.forEach((r) => dumpRoute(r, replace: route.path)); tabs--; } - if (header != null && header.isNotEmpty) buf.writeln(header); + dumpRouter(this); - dumpRoute(root); (callback ?? print)(buf.toString()); } @@ -184,26 +126,104 @@ class Router extends Extensible { /// /// Returns the created route. /// You can also register middleware within the router. - Route group(Pattern path, void callback(Router router), + SymlinkRoute group(Pattern path, void callback(Router router), {Iterable middleware: const [], - String method: "*", String name: null, String namespace: null}) { - final route = - root.child(path, handlers: middleware, method: method, name: name); - final router = new Router(root: route); + final router = new Router(debug: debug).._middleware.addAll(middleware); callback(router); - // Let's copy middleware, heeding the optional middleware namespace. - String middlewarePrefix = namespace != null ? "$namespace." : ""; + return mount(path, router, namespace: namespace).._name = name; + } - Map copiedMiddleware = new Map.from(router.requestMiddleware); - for (String middlewareName in copiedMiddleware.keys) { - requestMiddleware["$middlewarePrefix$middlewareName"] = - copiedMiddleware[middlewareName]; + /// Generates a URI string based on the given input. + /// Handy when you have named routes. + /// + /// Each item in `linkParams` should be a [Route], + /// `String` or `Map`. + /// + /// Strings should be route names, namespaces, or paths. + /// Maps should be parameters, which will be filled + /// into the previous route. + /// + /// Paths and segments should correspond to the way + /// you declared them. + /// + /// For example, if you declared a route group on + /// `'users/:id'`, it would not be resolved if you + /// passed `'users'` in [linkParams]. + /// + /// Leading and trailing slashes are automatically + /// removed. + /// + /// Set [absolute] to `true` to insert a forward slash + /// before the generated path. + /// + /// Example: + /// ```dart + /// router.navigate(['users/:id', {'id': '1337'}, 'profile']); + /// ``` + String navigate(List linkParams, {bool absolute: true}) { + final List segments = []; + Router search = this; + Route lastRoute; + + for (final param in linkParams) { + bool resolved = false; + + if (param is String) { + // Search by name + for (Route route in search.routes) { + if (route.name == param) { + segments.add(route.path.replaceAll(_straySlashes, '')); + lastRoute = route; + + if (route is SymlinkRoute) { + search = route.router; + } + + resolved = true; + break; + } + } + + // Search by path + for (Route route in search.routes) { + if (route.match(param) != null) { + segments.add(route.path.replaceAll(_straySlashes, '')); + lastRoute = route; + + if (route is SymlinkRoute) { + search = route.router; + } + + resolved = true; + break; + } + } + + if (!resolved) { + throw new RoutingException( + 'Cannot resolve route for link param "$param".'); + } + } else if (param is Route) { + segments.add(param.path.replaceAll(_straySlashes, '')); + } else if (param is Map) { + if (lastRoute == null) { + throw new RoutingException( + 'Maps in link params must be preceded by a Route or String.'); + } else { + segments.removeLast(); + segments.add(lastRoute.makeUri(param).replaceAll(_straySlashes, '')); + } + } else + throw new RoutingException( + 'Link param $param is not Route, String, or Map.'); } - return route; + return absolute + ? '/${segments.join('/').replaceAll(_straySlashes, '')}' + : segments.join('/'); } /// Assigns a middleware to a name for convenience. @@ -211,188 +231,68 @@ class Router extends Extensible { requestMiddleware[name] = middleware; } - /// Finds the first [Route] that matches the given path. - /// - /// You can pass an additional filter to determine which - /// routes count as matches. - Route resolveOnRoot(String path, {bool filter(Route route)}) => - root.resolve(path, filter: filter); - /// Finds the first [Route] that matches the given path, /// with the given method. - Route resolve(String path, {String method: 'GET'}) { - final String _path = path.replaceAll(_straySlashes, ''); - final segments = _path.split('/').where((str) => str.isNotEmpty); - _printDebug('Segments: $segments'); - return _resolve(root, _path, method, segments.isNotEmpty ? segments.first : '', segments.skip(1)); + RoutingResult resolve(String fullPath, String path, {String method: 'GET'}) { + final cleanFullPath = fullPath.replaceAll(_straySlashes, ''); + final cleanPath = path.replaceAll(_straySlashes, ''); + + for (Route route in routes) { + if (route is SymlinkRoute && route._head != null) { + final match = route._head.firstMatch(cleanFullPath); + + if (match != null) { + final tail = cleanPath + .replaceFirst(match[0], '') + .replaceAll(_straySlashes, ''); + _printDebug('Matched head "${match[0]}" to $route. Tail: "$tail"'); + final nested = + route.router.resolve(cleanFullPath, tail, method: method); + return new RoutingResult( + match: match, + nested: nested, + params: route.parseParameters(cleanPath), + sourceRoute: route, + sourceRouter: this, + tail: tail); + } + } else if (route.method == '*' || route.method == method) { + final match = route.match(cleanPath); + + if (match != null) { + return new RoutingResult( + match: match, + params: route.parseParameters(cleanPath), + sourceRoute: route, + sourceRouter: this); + } + } + } + + return null; } /// 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); + Iterable resolveAll(String fullPath, String path, + {String method: 'GET'}) { + final router = clone(); + final List results = []; + var result = router.resolve(fullPath, path, method: method); - while (resolved != null) { - try { - routes.add(resolved); - router.root._children.remove(resolved); - - resolved = router.resolve(path, method: method); - } catch (e) { - break; - } + while (result != null) { + results.add(result); + result.deepestRouter._routes.remove(result.deepestRoute); + result = router.resolve(fullPath, path, method: method); } - return routes.where((route) => route != null); + return results; } _validHead(RegExp rgx) { return !rgx.hasMatch(''); } - _resolve(Route ref, String fullPath, String method, String head, - Iterable tail) { - _printDebug('$method $fullPath on $ref: head: $head, tail: ${tail.join( - '/')}'); - - // Does the index route match? - if (ref.matcher.hasMatch(fullPath)) { - final index = ref.indexRoute; - - for (Route child in ref.allIndices) { - _printDebug('Possible index: $child'); - - if (child == child.indexRoute && ['*', method].contains(child.method)) { - _printDebug('Possible index was exact match: $child'); - return child; - } - - final resolved = _resolve(child, fullPath, method, head, tail); - - if (resolved != null) { - _printDebug('Resolved from possible index: $resolved'); - return resolved; - } else - _printDebug('Possible index returned null: $child'); - } - - if (['*', method].contains(index.method)) { - return index; - } - } else { - // Now, let's check if any route's head matches the - // given head. If so, we try to resolve with that - // given head. If so, we try to resolve with that - // route, using a head corresponding to the one we - // matched. - for (Route child in ref.children) { - if (child._head != null && - child._head.hasMatch(fullPath) && - _validHead(child._head)) { - final newHead = child._head - .firstMatch(fullPath) - .group(0) - .replaceAll(_straySlashes, ''); - final newTail = fullPath - .replaceAll(child._head, '') - .replaceAll(_straySlashes, '') - .split('/') - .where((str) => str.isNotEmpty); - final resolved = _resolve(child, fullPath, method, newHead, newTail); - - if (resolved != null) { - _printDebug( - 'Head match: $resolved from head: ${child._head.pattern}'); - return resolved; - } - } else if (child._head != null) { - _printDebug( - 'Head ${child._head.pattern} on $child failed to match $fullPath'); - } - } - - // Try to match children by full path - for (Route child in ref.children) { - if (child.matcher.hasMatch(fullPath)) { - final resolved = _resolve(child, fullPath, method, head, tail); - - if (resolved != null) { - return resolved; - } - } else { - _printDebug( - 'Could not match full path $fullPath to matcher ${child.matcher.pattern}.'); - } - } - } - - if (tail.isEmpty) - return null; - else { - return _resolve( - ref, fullPath, method, head + '/' + tail.first, tail.skip(1)); - } - } - - /// 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); - - if (normalize) this.normalize(); - - _flatten(Route parent, Route route) { - // if (route.children.isNotEmpty && route.method == '*') return; - - final r = new Route._base(); - - r - .._handlers.addAll(route.handlerSequence) - .._head = route._head - .._matcher = route.matcher - .._method = route.method - .._name = route.name - .._parent = route.parent // router.root - .._path = route.path; - - // New matcher - final part1 = parent.matcher.pattern - .replaceAll(_rgxStart, '') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, '') - .replaceAll(_straySlashes, ''); - final part2 = route.matcher.pattern - .replaceAll(_rgxStart, '') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, '') - .replaceAll(_straySlashes, ''); - - final m = '$part1\\/$part2'.replaceAll(_rgxStraySlashes, ''); - - // r._matcher = new RegExp('^$m\$'); - _printDebug('Matcher of flattened route: ${r.matcher.pattern}'); - - router.root._children.add(r); - route.children.forEach((child) => _flatten(route, child)); - } - - root._children.forEach((child) => _flatten(root, child)); - return router; - } - /// Incorporates another [Router]'s routes into this one's. /// /// If `hooked` is set to `true` and a [Service] is provided, @@ -403,7 +303,7 @@ class Router extends Extensible { /// For example, if the [Router] has a middleware 'y', and the `namespace` /// is 'x', then that middleware will be available as 'x.y' in the main router. /// These namespaces can be nested. - void mount(Pattern path, Router router, + SymlinkRoute mount(Pattern path, Router router, {bool hooked: true, String namespace: null}) { // Let's copy middleware, heeding the optional middleware namespace. String middlewarePrefix = namespace != null ? "$namespace." : ""; @@ -414,91 +314,11 @@ class Router extends Extensible { copiedMiddleware[middlewareName]; } - // final route = root.addChild(router.root, join: false); - final route = root.child(path, debug: debug).addChild(router.root); - route.debug = debug; + final route = new SymlinkRoute(path, path, _mounted[path] = router); + _routes.add(route); + route._head = new RegExp(route.matcher.pattern.replaceAll(_rgxEnd, '')); - if (path is! RegExp) { - // Correct mounted path manually... - final clean = route.matcher.pattern - .replaceAll(_rgxStart, '') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, ''); - route._matcher = new RegExp('^$clean\$'); - - final _path = path.toString().replaceAll(_straySlashes, ''); - - _migrateRoute(Route r) { - r._path = '$_path/${r.path}'.replaceAll(_straySlashes, ''); - var m = r.matcher.pattern - .replaceAll(_rgxStart, '') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, '') - .replaceAll(_straySlashes, ''); - - final m1 = _matcherify(_path) - .replaceAll(_rgxStart, '') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, '') - .replaceAll(_straySlashes, ''); - - m = '$m1/$m' - .replaceAll(_rgxStraySlashes, '') - .replaceAll(_straySlashes, ''); - - r._matcher = new RegExp('^$m\$'); - _printDebug( - 'New matcher on route in mounted router: ${r.matcher.pattern}'); - - if (r._head != null) { - final head = r._head.pattern - .replaceAll(_rgxStart, '') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, '') - .replaceAll('\\/', '/') - .replaceAll(_straySlashes, ''); - r._head = new RegExp(_matcherify('$_path/$head') - .replaceAll(_rgxEnd, '') - .replaceAll(_rgxStraySlashes, '')); - _printDebug('Head of migrated route: ${r._head.pattern}'); - } - - r.children.forEach(_migrateRoute); - } - - route.children.forEach(_migrateRoute); - } - } - - /// Removes empty routes that could complicate route resolution. - void normalize() { - _printDebug('Normalizing route tree...'); - - _normalize(Route route, int index) { - var merge = route.path.replaceAll(_straySlashes, '').isEmpty && - route.children.isNotEmpty; - merge = merge || route.children.length == 1; - - if (merge) { - _printDebug('Erasing this route: $route'); - // 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); - } - - for (int i = 0; i < route.children.length; i++) { - _normalize(route.children[i], i); - } - } - - for (int i = 0; i < root.children.length; i++) { - _normalize(root.children[i], i); - } + return route.._name = namespace; } /// Adds a route that responds to any request matching the given path. @@ -541,10 +361,3 @@ class Router extends Extensible { return addRoute('PUT', path, handler, middleware: middleware); } } - -class _RootRoute extends Route { - _RootRoute() : super("/", method: '*', name: ""); - - @override - String toString() => "ROOT"; -} diff --git a/lib/src/routing_result.dart b/lib/src/routing_result.dart new file mode 100644 index 00000000..9d147eae --- /dev/null +++ b/lib/src/routing_result.dart @@ -0,0 +1,47 @@ +part of angel_route.src.router; + +class RoutingResult { + final Match match; + final RoutingResult nested; + final Map params = {}; + final Route sourceRoute; + final Router sourceRouter; + final String tail; + + RoutingResult get deepest { + var search = this; + + while (search.nested != null) search = search.nested; + + return search; + } + + Route get deepestRoute => deepest.sourceRoute; + Router get deepestRouter => deepest.sourceRouter; + + List get handlers { + return []..addAll(sourceRouter.middleware)..addAll(sourceRoute.handlers); + } + + List get allHandlers { + final handlers = []; + var search = this; + + while (search != null) { + handlers.addAll(search.handlers); + search = search.nested; + } + + return handlers; + } + + RoutingResult( + {this.match, + Map params: const {}, + this.nested, + this.sourceRoute, + this.sourceRouter, + this.tail}) { + this.params.addAll(params ?? {}); + } +} diff --git a/lib/src/symlink_route.dart b/lib/src/symlink_route.dart new file mode 100644 index 00000000..f0618614 --- /dev/null +++ b/lib/src/symlink_route.dart @@ -0,0 +1,10 @@ +part of angel_route.src.router; + +/// Placeholder [Route] to serve as a symbolic link +/// to a mounted [Router]. +class SymlinkRoute extends Route { + final Pattern pattern; + final Router router; + + SymlinkRoute(Pattern path, this.pattern, this.router) : super(path); +} diff --git a/test/all_tests.browser.dart b/test/all_tests.browser.dart deleted file mode 100644 index fab39268..00000000 --- a/test/all_tests.browser.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:test/test.dart'; -import 'route/all_test.dart' as route; -import 'router/all_test.dart' as router; - -main() { - group('route', route.main); - group('router', router.main); -} \ No newline at end of file diff --git a/test/chain/all_test.dart b/test/chain/all_test.dart deleted file mode 100644 index f5161fa7..00000000 --- a/test/chain/all_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -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_test.dart b/test/method/all_test.dart deleted file mode 100644 index c8cad39a..00000000 --- a/test/method/all_test.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:angel_route/angel_route.dart'; -import 'package:test/test.dart'; - -main() { - var router = new Router(debug: true); - final getFoo = router.get('/foo', 'GET'); - final postFoo = router.post('/foo', 'POST'); - - Route getFooBar, postFooBar, patchFooBarId; - - router.group('/foo/bar', (router) { - getFooBar = router.get('/', 'GET'); - postFooBar = router.post('/', 'POST'); - patchFooBarId = router.patch('/:id([0-9]+)', 'PATCH'); - }); - - final Router books = new Router(); - - final getBooks = books.get('/', 'GET'); - final postBooks = books.post('/', 'POST'); - final getBooksFoo = books.get('/foo', 'GET'); - final postBooksFoo = books.post('/foo', 'POST'); - - Route getBooksChapters, - postBooksChapters, - getBooksChaptersReviews, - postBooksChaptersReviews; - - books.group('/:id/chapters', (router) { - getBooksChapters = router.get('/', 'GET'); - postBooksChapters = router.post('/', 'POST'); - - router.group('/:id([A-Za-z]+)/reviews', (router) { - getBooksChaptersReviews = router.get('/', 'GET'); - postBooksChaptersReviews = router.post('/', 'POST'); - }); - }); - - router.mount('/books', books); - router.normalize(); - - group('top level', () { - test('get', () => expect(router.resolve('/foo'), equals(getFoo))); - - test('post', () { - router.dumpTree(); - expect(router.resolve('/foo', method: 'POST'), equals(postFoo)); - }); - }); - - group('group', () { - test('get', () { - expect(router.resolve('/foo/bar'), equals(getFooBar)); - }); - - test('post', () { - expect(router.resolve('/foo/bar', method: 'POST'), equals(postFooBar)); - }); - - test('patch+id', () { - router.dumpTree(); - expect( - router.resolve('/foo/bar/2', method: 'PATCH'), equals(patchFooBarId)); - }); - - test('404', () { - expect(router.resolve('/foo/bar/A', method: 'PATCH'), isNull); - }); - }); - - group('mount', () { - group('no params', () { - test('get', () { - expect(router.resolve('/books'), equals(getBooks)); - expect(router.resolve('/books/foo'), equals(getBooksFoo)); - }); - - test('post', () { - expect(router.resolve('/books', method: 'POST'), equals(postBooks)); - expect( - router.resolve('/books/foo', method: 'POST'), equals(postBooksFoo)); - }); - }); - - group('with params', () { - test('1 param', () { - expect(router.resolve('/books/abc/chapters'), equals(getBooksChapters)); - expect(router.resolve('/books/abc/chapters', method: 'POST'), - equals(postBooksChapters)); - }); - - group('2 params', () { - setUp(router.dumpTree); - - test('get', () { - expect(router.resolve('/books/abc/chapters/ABC/reviews'), - equals(getBooksChaptersReviews)); - }); - - test('post', () { - expect( - router.resolve('/books/abc/chapters/ABC/reviews', method: 'POST'), - equals(postBooksChaptersReviews)); - }); - - test('404', () { - expect(router.resolve('/books/abc/chapters/1'), isNull); - expect(router.resolve('/books/abc/chapters/12'), isNull); - expect(router.resolve('/books/abc/chapters/13.!'), isNull); - }); - }); - }); - }); - - test('flatten', () { - router.dumpTree(header: 'BEFORE FLATTENING:'); - final flat = router..flatten(); - - for (Route route in flat.root.children) { - print('${route.method} ${route.path} => ${route.matcher.pattern}'); - } - }); -} diff --git a/test/navigate_test.dart b/test/navigate_test.dart new file mode 100644 index 00000000..95e80e17 --- /dev/null +++ b/test/navigate_test.dart @@ -0,0 +1,44 @@ +import 'package:angel_route/angel_route.dart'; +import 'package:test/test.dart'; + +main() { + final router = new Router(); + + router.get('/', 'GET').as('root'); + router.get('/user/:id', 'GET'); + router.get('/first/:first/last/:last', 'GET').as('full_name'); + + navigate(params) { + final uri = router.navigate(params); + print('Uri: $uri'); + return uri; + } + + router.dumpTree(showMatchers: true); + + group('top-level', () { + test('named', () { + expect(navigate(['root']), equals('/')); + }); + + test('params', () { + expect( + navigate([ + 'user/:id', + {'id': 1337} + ]), + equals('/user/1337')); + + expect( + navigate([ + 'full_name', + {'first': 'John', 'last': 'Smith'} + ]), + equals('/first/John/last/Smith')); + }); + + test('root', () { + expect(navigate(['/']), equals('/')); + }); + }); +} diff --git a/test/route/all_test.dart b/test/route/all_test.dart deleted file mode 100644 index 14a4988b..00000000 --- a/test/route/all_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:test/test.dart'; -import 'no_params.dart' as no_params; -import 'parse_params.dart' as parse_params; -import 'with_params.dart' as with_params; - -main() { - group('parse params', parse_params.main); - group('no params', no_params.main); - group('with params', with_params.main); -} \ No newline at end of file diff --git a/test/route/no_params.dart b/test/route/no_params.dart deleted file mode 100644 index cef4fe87..00000000 --- a/test/route/no_params.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:angel_route/angel_route.dart'; -import 'package:test/test.dart'; - -main() { - final foo = new Route.build('/foo', handlers: ['bar']); - final bar = foo.child('/bar'); - final baz = bar.child('//////baz//////', handlers: ['hello', 'world']); - - test('matching', () { - expect(foo.children.length, equals(1)); - expect(foo.handlers.length, equals(1)); - expect(foo.handlerSequence.length, equals(1)); - expect(foo.path, equals('foo')); - expect(foo.match('/foo'), isNotNull); - expect(foo.match('/bar'), isNull); - expect(foo.match('/foolish'), isNull); - expect(foo.parent, isNull); - expect(foo.absoluteParent, equals(foo)); - - expect(bar.path, equals('foo/bar')); - expect(bar.children.length, equals(1)); - expect(bar.handlers, isEmpty); - expect(bar.handlerSequence.length, equals(1)); - expect(bar.match('/foo/bar'), isNotNull); - expect(bar.match('/bar'), isNull); - expect(bar.match('/foo/bar/2'), isNull); - expect(bar.parent, equals(foo)); - expect(baz.absoluteParent, equals(foo)); - - expect(baz.children, isEmpty); - expect(baz.handlers.length, equals(2)); - expect(baz.handlerSequence.length, equals(3)); - expect(baz.path, equals('foo/bar/baz')); - expect(baz.match('/foo/bar/baz'), isNotNull); - expect(baz.match('/foo/bat/baz'), isNull); - expect(baz.match('/foo/bar/baz/1'), isNull); - expect(baz.parent, equals(bar)); - expect(baz.absoluteParent, equals(foo)); - }); - - test('hierarchy', () { - expect(foo.resolve('bar'), equals(bar)); - expect(foo.resolve('bar/baz'), equals(baz)); - - expect(bar.resolve('..'), equals(foo)); - expect(bar.resolve('/bar/baz'), equals(baz)); - expect(bar.resolve('../bar'), equals(bar)); - - expect(baz.resolve('..'), equals(bar)); - expect(baz.resolve('../..'), equals(foo)); - expect(baz.resolve('../baz'), equals(baz)); - expect(baz.resolve('../../bar'), equals(bar)); - expect(baz.resolve('../../bar/baz'), equals(baz)); - expect(baz.resolve('/bar'), equals(bar)); - expect(baz.resolve('/bar/baz'), equals(baz)); - }); -} diff --git a/test/route/parse_params.dart b/test/route/parse_params.dart deleted file mode 100644 index 58ec0adc..00000000 --- a/test/route/parse_params.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:angel_route/angel_route.dart'; -import 'package:test/test.dart'; - -p(x) { - print(x); - return x; -} - -main() { - final foo = new Route('/foo/:id'); - final bar = foo.child('/bar/:barId/baz'); - - test('make uri', () { - expect(p(foo.makeUri({'id': 1337})), equals('foo/1337')); - expect(p(bar.makeUri({'id': 1337, 'barId': 12})), - equals('foo/1337/bar/12/baz')); - }); - - test('parse', () { - final fooParams = foo.parseParameters('foo/1337/////'); - expect(p(fooParams), equals({'id': 1337})); - - final barParams = bar.parseParameters('/////foo/1337/bar/12/baz'); - expect(p(barParams), equals({'id': 1337, 'barId': 12})); - }); -} diff --git a/test/route/with_params.dart b/test/route/with_params.dart deleted file mode 100644 index 29fc8a39..00000000 --- a/test/route/with_params.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:angel_route/angel_route.dart'; -import 'package:test/test.dart'; - -main() { - final foo = new Router().root; - final fooById = foo.child(':id((?!bar)[0-9]+)', handlers: ['bar']); - final bar = fooById.child('bar'); - final baz = bar.child('//////baz//////', handlers: ['hello', 'world']); - final bazById = baz.child(':bazId([A-Za-z]+)?'); - new Router(root: foo).dumpTree(); - - test('matching', () { - expect(fooById.children.length, equals(1)); - expect(fooById.handlers.length, equals(1)); - expect(fooById.handlerSequence.length, equals(1)); - expect(fooById.path, equals(':id')); - expect(fooById.match('/2'), isNotNull); - expect(fooById.match('/aaa'), isNull); - expect(fooById.match('/bar'), isNull); - expect(fooById.match('lish'), isNull); - expect(fooById.parent, equals(foo)); - expect(fooById.absoluteParent, equals(foo)); - - expect(bar.path, equals(':id/bar')); - expect(bar.children.length, equals(1)); - expect(bar.handlers, isEmpty); - expect(bar.handlerSequence.length, equals(1)); - expect(bar.match('/2/bar'), isNotNull); - expect(bar.match('/bar'), isNull); - expect(bar.match('/a/bar'), isNull); - expect(bar.parent, equals(fooById)); - expect(baz.absoluteParent, equals(foo)); - - expect(baz.children.length, equals(1)); - expect(baz.handlers.length, equals(2)); - expect(baz.handlerSequence.length, equals(3)); - expect(baz.path, equals(':id/bar/baz')); - expect(baz.match('/2A/bar/baz'), isNull); - expect(baz.match('/2/bar/baz'), isNotNull); - expect(baz.match('/1337/bar/baz'), isNotNull); - expect(baz.match('/bat/baz'), isNull); - expect(baz.match('/bar/baz/1'), isNull); - expect(baz.parent, equals(bar)); - expect(baz.absoluteParent, equals(foo)); - }); - - test('hierarchy', () { - expect(fooById.resolve('/2'), equals(fooById)); - - expect(fooById.resolve('/2/bar'), equals(bar)); - expect(fooById.resolve('/bar'), isNull); - expect(fooById.resolve('/a/bar'), isNull); - expect(fooById.resolve('1337/bar/baz'), equals(baz)); - - expect(bar.resolve('..'), equals(fooById)); - - 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)); - - expect(baz.resolve('..'), equals(bar)); - expect(baz.resolve('../..'), equals(fooById)); - expect(baz.resolve('../baz'), equals(baz)); - expect(baz.resolve('../../bar'), equals(bar)); - expect(baz.resolve('../../bar/baz'), equals(baz)); - expect(baz.resolve('/2/bar'), equals(bar)); - expect(baz.resolve('/1337/bar/baz'), equals(baz)); - - expect(bar.resolve('/2/bar/baz/e'), equals(bazById)); - expect(bar.resolve('baz/e'), equals(bazById)); - expect(fooById.resolve('/2/bar/baz/e'), equals(bazById)); - expect(fooById.resolve('/2/bar/baz/2'), isNull); - expect(fooById.resolve('/2a/bar/baz/e'), isNull); - }); -} diff --git a/test/router/all_test.dart b/test/router/all_test.dart deleted file mode 100644 index 0343d4ef..00000000 --- a/test/router/all_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:angel_route/angel_route.dart'; -import 'package:test/test.dart'; -import 'fallback.dart' as fallback; - -final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - -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)); - - Route lower; - - router.group('/letter///', (router) { - lower = router - .get('/:id([A-Za-z])', (id) => ABC.indexOf(id[0])) - .child('////lower', handlers: [(String id) => id.toLowerCase()[0]]); - - lower.parent - .child('/upper', handlers: [(String id) => id.toUpperCase()[0]]); - }); - - router.dumpTree(header: "ROUTER TESTS"); - - test('extensible', () { - router['two'] = 2; - expect(router.two, equals(2)); - }); - - group('fallback', fallback.main); - - test('hierarchy', () { - expect(lower.absoluteParent, equals(router.root)); - expect(lower.parent.path, equals('letter/:id')); - expect(lower.resolve('../upper').path, equals('letter/:id/upper')); - expect(lower.resolve('/user/34/detail'), equals(deleteUserById)); - expect(deleteUserById.resolve('../../fizz'), equals(fizz)); - }, skip: 'Hierarchy is deprecated.'); - - test('resolve', () { - expect(router.resolveOnRoot('/'), equals(indexRoute)); - expect(router.resolveOnRoot('user/1337/detail'), equals(deleteUserById)); - expect(router.resolveOnRoot('/user/1337/detail'), equals(deleteUserById)); - expect(router.resolveOnRoot('letter/a/lower'), equals(lower)); - expect(router.resolveOnRoot('letter/2/lower'), isNull); - }); -} diff --git a/test/router/fallback.dart b/test/router/fallback.dart deleted file mode 100644 index eee93c6c..00000000 --- a/test/router/fallback.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:angel_route/angel_route.dart'; -import 'package:test/test.dart'; - -bool checkPost(Route route) => route.method == "POST"; - -main() { - final router = new Router(); - - final userById = router.group('/user', (router) { - router.get('/:id', (id) => 'User $id'); - }).resolve(':id'); - - final fallback = router.get('*', () => 'fallback'); - - test('resolve', () { - expect(router.resolveOnRoot('/foo'), equals(fallback)); - expect(router.resolveOnRoot('/user/:id'), equals(userById)); - expect(router.resolveOnRoot('/user/:id', filter: checkPost), isNull); - }); -} diff --git a/test/server/all_test.dart b/test/server/all_test.dart deleted file mode 100644 index 7758e791..00000000 --- a/test/server/all_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -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.path, method: request.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)); - }); - }); -} diff --git a/test/server_test.dart b/test/server_test.dart new file mode 100644 index 00000000..263aeccf --- /dev/null +++ b/test/server_test.dart @@ -0,0 +1,126 @@ +import 'dart:convert'; +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; + final people = [ + {'name': 'John Smith'} + ]; + final Router router = new Router(debug: true); + HttpServer server; + String url; + + router.get('/', (req, res) { + res.write('Root'); + return false; + }); + + router.get('/hello', (req, res) { + res.write('World'); + return false; + }); + + router.group('/people', (router) { + router.get('/', (req, res) { + res.write(JSON.encode(people)); + return false; + }); + + router.group('/:id', (router) { + router.get('/', (req, res) { + // In a real application, we would take the param, + // but not here... + res.write(JSON.encode(people.first)); + return false; + }); + + router.get('/name', (req, res) { + // In a real application, we would take the param, + // but not here... + res.write(JSON.encode(people.first['name'])); + return false; + }); + }); + }); + + setUp(() async { + client = new http.Client(); + + router.dumpTree(); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); + url = 'http://${server.address.address}:${server.port}'; + + server.listen((req) async { + final res = req.response; + + // Easy middleware pipeline + final results = router.resolveAll(req.uri.toString(), req.uri.toString(), + method: req.method); + final pipeline = new MiddlewarePipeline(results); + + if (pipeline.handlers.isEmpty) { + res + ..statusCode = HttpStatus.NOT_FOUND + ..writeln('404 Not Found'); + } else { + for (final handler in pipeline.handlers) { + if (!await handler(req, res)) break; + } + } + + await res.close(); + }); + }); + + tearDown(() async { + await server.close(force: true); + client.close(); + client = null; + url = null; + }); + + group('top-level', () { + group('get', () { + test('root', () async { + final res = await client.get(url); + print('Response: ${res.body}'); + expect(res.body, equals('Root')); + }); + + test('path', () async { + final res = await client.get('$url/hello'); + print('Response: ${res.body}'); + expect(res.body, equals('World')); + }); + }); + }); + + group('group', () { + group('top-level', () { + test('root', () async { + final res = await client.get('$url/people'); + print('Response: ${res.body}'); + expect(JSON.decode(res.body), equals(people)); + }); + + group('param', () { + test('root', () async { + final res = await client.get('$url/people/0'); + print('Response: ${res.body}'); + expect(JSON.decode(res.body), equals(people.first)); + }); + + test('path', () async { + final res = await client.get('$url/people/0/name'); + print('Response: ${res.body}'); + expect(JSON.decode(res.body), equals(people.first['name'])); + }); + }); + }); + }); + + group('use', () {}); +} diff --git a/test/use.dart b/test/use.dart deleted file mode 100644 index 383f553b..00000000 --- a/test/use.dart +++ /dev/null @@ -1,35 +0,0 @@ -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); - 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.mount('child', child); - parent.dumpTree(header: tattleAll([parent, child, a])); - - group('no params', () { - test('resolve', () { - expect(child.resolveOnRoot('a'), equals(a)); - expect(child.resolveOnRoot('b'), equals(b)); - expect(child.resolveOnRoot('b/c'), equals(c)); - - expect(parent.resolveOnRoot('child/a'), equals(a)); - expect(parent.resolveOnRoot('a'), isNull); - expect(parent.resolveOnRoot('child/b'), equals(b)); - expect(parent.resolveOnRoot('child/b/c'), equals(c)); - }); - }); -} diff --git a/web/hash/basic.dart b/web/hash/basic.dart deleted file mode 100644 index b5ba5e99..00000000 --- a/web/hash/basic.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:angel_route/browser.dart'; -import '../shared/basic.dart'; - -main() => basic(new BrowserRouter(hash: true)); - diff --git a/web/hash/basic.html b/web/hash/basic.html deleted file mode 100644 index e2e0e86b..00000000 --- a/web/hash/basic.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Hash Router - - - - -

No Active Route

-Handler Sequence: -
    -
  • (empty)
  • -
- - - - \ No newline at end of file diff --git a/web/push_state/basic.dart b/web/push_state/basic.dart deleted file mode 100644 index d889a70d..00000000 --- a/web/push_state/basic.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:angel_route/browser.dart'; -import '../shared/basic.dart'; - -main() => basic(new BrowserRouter()); - diff --git a/web/push_state/basic.html b/web/push_state/basic.html deleted file mode 100644 index c2b6faca..00000000 --- a/web/push_state/basic.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Push State Router - - - - -

No Active Route

-Handler Sequence: -
    -
  • (empty)
  • -
- - - - \ No newline at end of file diff --git a/web/shared/basic.dart b/web/shared/basic.dart deleted file mode 100644 index e897e3a6..00000000 --- a/web/shared/basic.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:html'; -import 'package:angel_route/browser.dart'; - -basic(BrowserRouter router) { - final $h1 = window.document.querySelector('h1'); - final $ul = window.document.getElementById('handlers'); - - router.onRoute.listen((route) { - if (route == null) { - $h1.text = 'No Active Route'; - $ul.children - ..clear() - ..add(new LIElement()..text = '(empty)'); - } else { - $h1.text = 'Active Route: ${route.path}'; - $ul.children - ..clear() - ..addAll(route.handlerSequence - .map((handler) => new LIElement()..text = handler.toString())); - } - }); - - router.get('a', 'a handler'); - - router.group('b', (router) { - print(router.root); - router.get('a', 'b/a handler'); - router.get('b', 'b/b handler', middleware: ['b/b middleware']); - }, middleware: ['b middleware']); - - router.get('c', 'c handler'); - - router.dumpTree(); -} \ No newline at end of file