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:
-
-
-
-
-
\ 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:
-
-
-
-
-
\ 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