Need to fix clone...

This commit is contained in:
thosakwe 2016-11-25 18:22:33 -05:00
parent 5897a9839b
commit 5d5ac7bef9
38 changed files with 465 additions and 1390 deletions

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Route Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/route/all_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Router Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/router/all_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Server Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Server Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/server/all_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Chain" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/chain/all_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Fallback" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/router/fallback.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Method Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/method/all_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Navigate Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/navigate_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="No Params" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/route/no_params.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Parse Params" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/route/parse_params.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/server_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Use" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/use.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="With Params" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/route/with_params.dart" />
<method />
</configuration>
</component>

View file

@ -1,5 +1,6 @@
/// A powerful, isomorphic routing library for Dart.
library angel_route; library angel_route;
export 'src/extensible.dart';
export 'src/middleware_pipeline.dart';
export 'src/router.dart'; export 'src/router.dart';
export 'src/routing_exception.dart'; export 'src/routing_exception.dart';

View file

@ -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<Route> 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<Route> _onRoute = new StreamController<Route>();
Route get currentRoute => _current;
@override
Stream<Route> 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);
});
}
}

View file

@ -0,0 +1,17 @@
import 'router.dart';
class MiddlewarePipeline {
final List<RoutingResult> routingResults;
List get handlers {
final handlers = [];
for (RoutingResult result in routingResults) {
handlers.addAll(result.allHandlers);
}
return handlers;
}
MiddlewarePipeline(this.routingResults);
}

View file

@ -351,7 +351,7 @@ class Route {
final pathString = _pathify(path).replaceAll(new RegExp('\/'), r'\/'); final pathString = _pathify(path).replaceAll(new RegExp('\/'), r'\/');
Iterable<Match> matches = _param.allMatches(pathString); Iterable<Match> matches = _param.allMatches(pathString);
_printDebug( _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++) { for (int i = 0; i < matches.length && i < values.length; i++) {
Match match = matches.elementAt(i); Match match = matches.elementAt(i);

View file

@ -2,8 +2,9 @@ library angel_route.src.router;
import 'extensible.dart'; import 'extensible.dart';
import 'routing_exception.dart'; import 'routing_exception.dart';
part 'symlink_route.dart';
part 'route.dart'; part 'route.dart';
part 'routing_result.dart';
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _rgxEnd = new RegExp(r'\$+$'); final RegExp _rgxEnd = new RegExp(r'\$+$');
@ -14,23 +15,27 @@ final RegExp _slashDollar = new RegExp(r'/+\$');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :) /// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
class Router extends Extensible { class Router {
Route _root; final List _middleware = [];
final Map<Pattern, Router> _mounted = {};
final List<Route> _routes = [];
/// Set to `true` to print verbose debug output when interacting with this route. /// Set to `true` to print verbose debug output when interacting with this route.
bool debug = false; bool debug = false;
List get middleware => new List.unmodifiable(_middleware);
Map<Pattern, Router> get mounted =>
new Map<Pattern, Router>.unmodifiable(_mounted);
/// Additional filters to be run on designated requests. /// Additional filters to be run on designated requests.
Map<String, dynamic> requestMiddleware = {}; Map<String, dynamic> requestMiddleware = {};
/// The single [Route] that serves as the root of the hierarchy. List<Route> get routes => new List<Route>.unmodifiable(_routes);
Route get root => _root;
/// Provide a `root` to make this Router revolve around a pre-defined route. /// Provide a `root` to make this Router revolve around a pre-defined route.
/// Not recommended. /// Not recommended.
Router({this.debug: false, Route root}) { Router({this.debug: false});
_root = (_root = root ?? new _RootRoute())..debug = debug;
}
void _printDebug(msg) { void _printDebug(msg) {
if (debug == true) print(msg); if (debug == true) print(msg);
@ -40,142 +45,79 @@ class Router extends Extensible {
/// for requests with the given method (case-insensitive). /// for requests with the given method (case-insensitive).
/// Provide '*' as the method to respond to all methods. /// Provide '*' as the method to respond to all methods.
Route addRoute(String method, Pattern path, Object handler, Route addRoute(String method, Pattern path, Object handler,
{List middleware}) { {List middleware: const []}) {
List handlers = []; // Check if any mounted routers can match this
final handlers = [handler];
handlers if (middleware != null) handlers.addAll(middleware);
..addAll(middleware ?? [])
..add(handler);
if (path is RegExp) { final route =
return root.child(path, debug: debug, handlers: handlers, method: method); new Route(path, debug: debug, method: method, handlers: handlers);
} else { _routes.add(route);
// if (path.toString().replaceAll(_straySlashes, '').isEmpty || true) { return route;
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;
} */
} }
/// Returns a [Router] with a duplicated version of this tree. /// Returns a [Router] with a duplicated version of this tree.
Router clone({bool normalize: true}) { Router clone() {
final router = new Router(debug: debug); final router = new Router(debug: debug);
final newMounted = new Map.from(mounted);
_copy(Route route, Route parent) { for (Route route in routes) {
final r = route.clone(); if (route is! SymlinkRoute) {
parent._children.add(r.._parent = parent); router._routes.add(route.clone());
} else if (route is SymlinkRoute) {
route.children.forEach((child) => _copy(child, r)); router._routes.add(new SymlinkRoute(route.path, route.pattern,
newMounted[route.pattern] = route.router.clone()));
}
} }
root.children.forEach((child) => _copy(child, router.root)); return router.._mounted.addAll(newMounted);
if (normalize) router.normalize();
return router;
} }
/// Creates a visual representation of the route hierarchy and /// Creates a visual representation of the route hierarchy and
/// passes it to a callback. If none is provided, `print` is called. /// passes it to a callback. If none is provided, `print` is called.
void dumpTree( void dumpTree(
{callback(String tree), {callback(String tree),
header: 'Dumping route tree:', String header: 'Dumping route tree:',
tab: ' ', String tab: ' ',
showMatchers: false}) { bool showMatchers: false}) {
var tabs = 0;
final buf = new StringBuffer(); final buf = new StringBuffer();
int tabs = 0;
void dumpRoute(Route route, {Pattern replace: null}) { if (header != null && header.isNotEmpty) {
for (var i = 0; i < tabs; i++) buf.write(tab); buf.writeln(header);
if (route == root)
buf.writeln('(root)');
else {
buf.write('- ${route.method} ');
var p =
replace != null ? route.path.replaceAll(replace, '') : route.path;
p = p.replaceAll(_straySlashes, '');
if (p.isEmpty)
buf.write("'/'");
else
buf.write("'${p.replaceAll(_straySlashes, '')}'");
if (showMatchers) {
buf.write(' (matcher: ${route.matcher.pattern})');
} }
if (route.handlers.isNotEmpty) indent() {
buf.writeln(' => ${route.handlers.length} handler(s)'); for (int i = 0; i < tabs; i++) buf.write(tab);
else
buf.writeln();
} }
dumpRouter(Router router) {
indent();
buf.writeln('- <root>');
tabs++; tabs++;
route.children.forEach((r) => dumpRoute(r, replace: route.path));
for (Route route in router.routes) {
indent();
buf.write('- ${route.path.isNotEmpty ? route.path : '/'}');
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--; tabs--;
} }
if (header != null && header.isNotEmpty) buf.writeln(header); dumpRouter(this);
dumpRoute(root);
(callback ?? print)(buf.toString()); (callback ?? print)(buf.toString());
} }
@ -184,26 +126,104 @@ class Router extends Extensible {
/// ///
/// Returns the created route. /// Returns the created route.
/// You can also register middleware within the router. /// 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 [], {Iterable middleware: const [],
String method: "*",
String name: null, String name: null,
String namespace: null}) { String namespace: null}) {
final route = final router = new Router(debug: debug).._middleware.addAll(middleware);
root.child(path, handlers: middleware, method: method, name: name);
final router = new Router(root: route);
callback(router); callback(router);
// Let's copy middleware, heeding the optional middleware namespace. return mount(path, router, namespace: namespace).._name = name;
String middlewarePrefix = namespace != null ? "$namespace." : "";
Map copiedMiddleware = new Map.from(router.requestMiddleware);
for (String middlewareName in copiedMiddleware.keys) {
requestMiddleware["$middlewarePrefix$middlewareName"] =
copiedMiddleware[middlewareName];
} }
return route; /// 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<String, dynamic>`.
///
/// 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<String> 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<String, dynamic>) {
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<String, dynamic>.');
}
return absolute
? '/${segments.join('/').replaceAll(_straySlashes, '')}'
: segments.join('/');
} }
/// Assigns a middleware to a name for convenience. /// Assigns a middleware to a name for convenience.
@ -211,188 +231,68 @@ class Router extends Extensible {
requestMiddleware[name] = middleware; 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, /// Finds the first [Route] that matches the given path,
/// with the given method. /// with the given method.
Route resolve(String path, {String method: 'GET'}) { RoutingResult resolve(String fullPath, String path, {String method: 'GET'}) {
final String _path = path.replaceAll(_straySlashes, ''); final cleanFullPath = fullPath.replaceAll(_straySlashes, '');
final segments = _path.split('/').where((str) => str.isNotEmpty); final cleanPath = path.replaceAll(_straySlashes, '');
_printDebug('Segments: $segments');
return _resolve(root, _path, method, segments.isNotEmpty ? segments.first : '', segments.skip(1)); 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, /// Finds every possible [Route] that matches the given path,
/// with the given method. /// with the given method.
/// Iterable<RoutingResult> resolveAll(String fullPath, String path,
/// This is preferable to [resolve]. {String method: 'GET'}) {
/// Keep in mind that this function uses either a [linearClone] or a [clone], and thus final router = clone();
/// will not return the same exact routes from the original tree. final List<RoutingResult> results = [];
Iterable<Route> resolveAll(String path, var result = router.resolve(fullPath, path, method: method);
{bool linear: true, String method: 'GET', bool normalizeClone: true}) {
final router = linear
? linearClone(normalize: normalizeClone)
: clone(normalize: normalizeClone);
final routes = [];
var resolved = router.resolve(path, method: method);
while (resolved != null) { while (result != null) {
try { results.add(result);
routes.add(resolved); result.deepestRouter._routes.remove(result.deepestRoute);
router.root._children.remove(resolved); result = router.resolve(fullPath, path, method: method);
resolved = router.resolve(path, method: method);
} catch (e) {
break;
}
} }
return routes.where((route) => route != null); return results;
} }
_validHead(RegExp rgx) { _validHead(RegExp rgx) {
return !rgx.hasMatch(''); return !rgx.hasMatch('');
} }
_resolve(Route ref, String fullPath, String method, String head,
Iterable<String> 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. /// Incorporates another [Router]'s routes into this one's.
/// ///
/// If `hooked` is set to `true` and a [Service] is provided, /// 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` /// 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. /// is 'x', then that middleware will be available as 'x.y' in the main router.
/// These namespaces can be nested. /// These namespaces can be nested.
void mount(Pattern path, Router router, SymlinkRoute mount(Pattern path, Router router,
{bool hooked: true, String namespace: null}) { {bool hooked: true, String namespace: null}) {
// Let's copy middleware, heeding the optional middleware namespace. // Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = namespace != null ? "$namespace." : ""; String middlewarePrefix = namespace != null ? "$namespace." : "";
@ -414,91 +314,11 @@ class Router extends Extensible {
copiedMiddleware[middlewareName]; copiedMiddleware[middlewareName];
} }
// final route = root.addChild(router.root, join: false); final route = new SymlinkRoute(path, path, _mounted[path] = router);
final route = root.child(path, debug: debug).addChild(router.root); _routes.add(route);
route.debug = debug; route._head = new RegExp(route.matcher.pattern.replaceAll(_rgxEnd, ''));
if (path is! RegExp) { return route.._name = namespace;
// 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);
}
} }
/// Adds a route that responds to any request matching the given path. /// 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); return addRoute('PUT', path, handler, middleware: middleware);
} }
} }
class _RootRoute extends Route {
_RootRoute() : super("/", method: '*', name: "<root>");
@override
String toString() => "ROOT";
}

View file

@ -0,0 +1,47 @@
part of angel_route.src.router;
class RoutingResult {
final Match match;
final RoutingResult nested;
final Map<String, dynamic> 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<String, dynamic> params: const {},
this.nested,
this.sourceRoute,
this.sourceRouter,
this.tail}) {
this.params.addAll(params ?? {});
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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'));
});
});
});
}

View file

@ -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}');
}
});
}

44
test/navigate_test.dart Normal file
View file

@ -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('/'));
});
});
}

View file

@ -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);
}

View file

@ -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));
});
}

View file

@ -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}));
});
}

View file

@ -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);
});
}

View file

@ -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);
});
}

View file

@ -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);
});
}

View file

@ -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<bool> 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));
});
});
}

126
test/server_test.dart Normal file
View file

@ -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', () {});
}

View file

@ -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));
});
});
}

View file

@ -1,5 +0,0 @@
import 'package:angel_route/browser.dart';
import '../shared/basic.dart';
main() => basic(new BrowserRouter(hash: true));

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Hash Router</title>
<style>
#routes li {
display: inline;
margin-right: 1em;
}
</style>
</head>
<body>
<ul id="routes">
<li><a href="/a">Route A</a></li>
<li><a href="/b">Route B</a></li>
<li><a href="/b/a">Route B/A</a></li>
<li><a href="/b/b">Route B/B</a></li>
<li><a href="/c">Route C</a></li>
</ul>
<h1>No Active Route</h1>
<i>Handler Sequence:</i>
<ul id="handlers">
<li>(empty)</li>
</ul>
<script src="basic.dart" type="application/dart"></script>
<script src="packages/browser/dart.js"></script>
</body>
</html>

View file

@ -1,5 +0,0 @@
import 'package:angel_route/browser.dart';
import '../shared/basic.dart';
main() => basic(new BrowserRouter());

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Push State Router</title>
<style>
#routes li {
display: inline;
margin-right: 1em;
}
</style>
</head>
<body>
<ul id="routes">
<li><a href="/a">Route A</a></li>
<li><a href="/b">Route B</a></li>
<li><a href="/b/a">Route B/A</a></li>
<li><a href="/b/b">Route B/B</a></li>
<li><a href="/c">Route C</a></li>
</ul>
<h1>No Active Route</h1>
<i>Handler Sequence:</i>
<ul id="handlers">
<li>(empty)</li>
</ul>
<script src="basic.dart" type="application/dart"></script>
<script src="packages/browser/dart.js"></script>
</body>
</html>

View file

@ -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();
}