Finally fixed router!

This commit is contained in:
thosakwe 2016-11-21 22:31:09 -05:00
parent 5241e66eff
commit 34a234cb4d
24 changed files with 385 additions and 101 deletions

View file

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

View file

@ -0,0 +1,6 @@
<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_tests.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +1,6 @@
<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/router/use.dart" />
<option name="filePath" value="$PROJECT_DIR$/test/use.dart" />
<method />
</configuration>
</component>

1
.travis.yml Normal file
View file

@ -0,0 +1 @@
language: dart

View file

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

View file

@ -52,6 +52,11 @@ class _BrowserRouterImpl extends Router implements BrowserRouter {
throw new RoutingException.noSuchRoute(path);
}
@override
void listen() {
normalize();
}
void prepareAnchors() {
final anchors = window.document.querySelectorAll('a:not([dynamic])');
@ -85,9 +90,10 @@ class _HashRouter extends _BrowserRouterImpl {
@override
void listen() {
super.listen();
window.onHashChange.listen((_) {
final path = window.location.hash.replaceAll(_hash, '');
final resolved = resolve(path);
final resolved = resolveOnRoot(path);
if (resolved == null || (path.isEmpty && resolved == root)) {
_onRoute.add(_current = null);
@ -115,6 +121,7 @@ class _PushStateRouter extends _BrowserRouterImpl {
@override
void listen() {
super.listen();
window.onPopState.listen((e) {
if (e.state is Map && e.state.containsKey('path')) {
final resolved = resolve(e.state['path']);

View file

@ -1,11 +1,4 @@
import 'extensible.dart';
import 'routing_exception.dart';
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _rgxEnd = new RegExp(r'\$+$');
final RegExp _rgxStart = new RegExp(r'^\^+');
final RegExp _rgxStraySlashes = new RegExp(r'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
part of angel_route.src.router;
String _matcherify(String path, {bool expand: true}) {
var p = path.replaceAll(new RegExp(r'/\*$'), "*").replaceAll('/', r'\/');
@ -48,6 +41,7 @@ String _pathify(String path) {
class Route {
final List<Route> _children = [];
final List _handlers = [];
RegExp _head;
RegExp _matcher;
String _method;
String _name;
@ -94,6 +88,14 @@ class Route {
return result;
}
/// Returns the [Route] instances that will respond to requests
/// to the index of this instance's path.
///
/// May return `this`.
Iterable<Route> get allIndices {
return children.where((r) => r.path.replaceAll(path, '').isEmpty);
}
/// Backtracks up the hierarchy, and builds
/// a sequential list of all handlers from both
/// this route, and every found parent route.
@ -128,6 +130,8 @@ class Route {
if (debug) print(msg);
}
Route._base();
Route(Pattern path,
{Iterable<Route> children: const [],
this.debug: false,
@ -190,8 +194,11 @@ class Route {
name: name);
}
var head = '';
for (int i = 0; i < segments.length; i++) {
final segment = segments[i];
head = (head + '/$segment').replaceAll(_straySlashes, '');
if (i == segments.length - 1) {
if (result == null) {
@ -206,6 +213,8 @@ class Route {
result = result.child(segment, debug: debug, method: "*");
}
}
result._head = new RegExp(_matcherify(head).replaceAll(_rgxEnd, ''));
}
}
@ -244,33 +253,16 @@ class Route {
parent._children.add(route
.._matcher = new RegExp('$pattern1$separator$pattern2')
.._head = new RegExp(_matcherify('$path1/$path2'.replaceAll(_straySlashes, '')).replaceAll(_rgxEnd, ''))
.._parent = parent
.._stub = child.matcher);
parent._printDebug(
"Joined '/$path1' and '/$path2', created stub: ${route._stub.pattern}");
"Joined '/$path1' and '/$path2', created head: ${route._head.pattern} and stub: ${route._stub.pattern}");
return route..debug = parent.debug || child.debug || debug;
}
Route _inherit(Route route) {
/*
final List<Route> _children = [];
final List _handlers = [];
RegExp _matcher;
String _method;
String _name;
Route _parent;
RegExp _parentResolver;
String _path;
String _pathified;
RegExp _resolver;
RegExp _stub;
*/
return route.._parent = this;
}
/// Calls [addChild] on all given routes.
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
return routes.map((route) => addChild(route, join: join)).toList();

View file

@ -1,7 +1,15 @@
library angel_route.src.router;
import 'extensible.dart';
import 'route.dart';
import 'routing_exception.dart';
part 'route.dart';
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _rgxEnd = new RegExp(r'\$+$');
final RegExp _rgxStart = new RegExp(r'^\^+');
final RegExp _rgxStraySlashes = new RegExp(r'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
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. :)
@ -20,7 +28,8 @@ class Router extends Extensible {
/// 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;
_root = (_root = root ?? new _RootRoute())
..debug = debug;
}
void _printDebug(msg) {
@ -40,7 +49,10 @@ class Router extends Extensible {
if (path is RegExp) {
return root.child(path, debug: debug, handlers: handlers, method: method);
} else if (path.toString().replaceAll(_straySlashes, '').isEmpty) {
} else if (path
.toString()
.replaceAll(_straySlashes, '')
.isEmpty) {
return root.child(path.toString(),
debug: debug, handlers: handlers, method: method);
} else {
@ -55,8 +67,8 @@ class Router extends Extensible {
return new Route('/', debug: debug, handlers: handlers, method: method)
..debug = debug;
} else {
result = resolve(segments[0],
(route) => route.method == method || route.method == '*');
result = resolveOnRoot(segments[0],
filter: (route) => route.method == method || route.method == '*');
if (result != null) {
if (segments.length > 1) {
@ -110,15 +122,17 @@ class Router extends Extensible {
final buf = new StringBuffer();
void dumpRoute(Route route, {Pattern replace: null}) {
for (var i = 0; i < tabs; i++) buf.write(tab);
for (var i = 0; i < tabs; i++)
buf.write(tab);
if (route == root)
buf.write('(root)');
buf.writeln('(root)');
else {
buf.write('- ${route.method} ');
final p =
var p =
replace != null ? route.path.replaceAll(replace, '') : route.path;
p = p.replaceAll(_straySlashes, '');
if (p.isEmpty)
buf.write("'/'");
@ -179,9 +193,130 @@ class Router extends Extensible {
///
/// You can pass an additional filter to determine which
/// routes count as matches.
Route resolve(String path, [bool filter(Route route)]) =>
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.first, segments.skip(1));
}
_validHead(RegExp rgx) {
return !rgx.hasMatch('');
}
_resolve(Route ref, String fullPath, String method, String head,
Iterable<String> tail) {
_printDebug(
'$method on $ref: path: $fullPath, 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 {
// 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;
}
}
}
// Now, let's check if any route's head matches the
// 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');
}
}
}
if (tail.isEmpty)
return null;
else {
return _resolve(
ref, fullPath, method, head + '/' + tail.first, tail.skip(1));
}
}
/// Returns a new Router in which the route tree has been
/// flattened into a linear list.
Router flatten() {
final router = new Router();
_flatten(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
.._parent = router.root
.._path = route.path;
router.root._children.add(r);
route.children.forEach(_flatten);
}
root._children.forEach(_flatten);
return router..debug = debug;
}
/// Incorporates another [Router]'s routes into this one's.
///
/// If `hooked` is set to `true` and a [Service] is provided,
@ -203,7 +338,64 @@ class Router extends Extensible {
copiedMiddleware[middlewareName];
}
root.child(path, debug: debug).addChild(router.root);
// final route = root.addChild(router.root, join: false);
final route = root.child(path, debug: debug).addChild(router.root);
route.debug = debug;
if (path is! RegExp) {
final _path = path.toString().replaceAll(_straySlashes, '');
_migrateRoute(Route r) {
r._path = '$_path/${r.path}'.replaceAll(_straySlashes, '');
var stripped = r.matcher.pattern
.replaceAll(_rgxStart, '')
.replaceAll(_rgxEnd, '')
.replaceAll(_rgxStraySlashes, '')
.replaceAll(_straySlashes, '');
stripped = '$_path/$stripped'.replaceAll(_straySlashes, '');
r._matcher = new RegExp('^$stripped\$');
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, ''));
_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) {
route.children.forEach(_normalize);
if (route.path
.replaceAll(_straySlashes, '')
.isEmpty &&
route.children.isNotEmpty) {
_printDebug('Erasing this route: $route');
route.parent._handlers.addAll(route.handlers);
for (Route child in route.children) {
route.parent._children.add(child.._parent = route.parent);
}
route.parent._children.remove(route);
}
}
root.children.forEach(_normalize);
}
/// Adds a route that responds to any request matching the given path.
@ -248,7 +440,7 @@ class Router extends Extensible {
}
class _RootRoute extends Route {
_RootRoute() : super("/", name: "<root>");
_RootRoute() : super("/", method: '*', name: "<root>");
@override
String toString() => "ROOT";

View file

@ -1,6 +1,6 @@
name: angel_route
description: A powerful, isomorphic routing library for Dart.
version: 1.0.0-dev+5
version: 1.0.0-dev+6
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_route
dev_dependencies:

View file

@ -1,10 +0,0 @@
import 'package:test/test.dart';
import 'route/all_tests.dart' as route;
import 'router/all_tests.dart' as router;
import 'server/all_tests.dart' as server;
main() {
group('route', route.main);
group('router', router.main);
group('server', server.main);
}

View file

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tests</title>
</head>
<body>
<script src="all_tests.browser.dart" type="application/dart"></script>
<script src="packages/browser/dart.js"></script>
</body>
</html>

123
test/method/all_tests.dart Normal file
View file

@ -0,0 +1,123 @@
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}');
}
});
}

View file

@ -1 +0,0 @@
../packages

View file

@ -1 +0,0 @@
../../packages

View file

@ -1,7 +1,6 @@
import 'package:angel_route/angel_route.dart';
import 'package:test/test.dart';
import 'fallback.dart' as fallback;
import 'use.dart' as use;
final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
@ -13,7 +12,8 @@ main() {
router.delete('/user/:id/detail', (id) => num.parse(id));
Route lower;
final letters = router.group('/letter///', (router) {
router.group('/letter///', (router) {
lower = router
.get('/:id([A-Za-z])', (id) => ABC.indexOf(id[0]))
.child('////lower', handlers: [(String id) => id.toLowerCase()[0]]);
@ -30,7 +30,6 @@ main() {
});
group('fallback', fallback.main);
test('group & use', use.main);
test('hierarchy', () {
expect(lower.absoluteParent, equals(router.root));
@ -41,10 +40,10 @@ main() {
});
test('resolve', () {
expect(router.resolve('/'), equals(indexRoute));
expect(router.resolve('user/1337/detail'), equals(deleteUserById));
expect(router.resolve('/user/1337/detail'), equals(deleteUserById));
expect(router.resolve('letter/a/lower'), equals(lower));
expect(router.resolve('letter/2/lower'), isNull);
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

@ -13,8 +13,8 @@ main() {
final fallback = router.get('*', () => 'fallback');
test('resolve', () {
expect(router.resolve('/foo'), equals(fallback));
expect(router.resolve('/user/:id'), equals(userById));
expect(router.resolve('/user/:id', checkPost), isNull);
expect(router.resolveOnRoot('/foo'), equals(fallback));
expect(router.resolveOnRoot('/user/:id'), equals(userById));
expect(router.resolveOnRoot('/user/:id', filter: checkPost), isNull);
});
}

View file

@ -1 +0,0 @@
../../packages

View file

@ -22,7 +22,7 @@ main() {
url = 'http://${server.address.address}:${server.port}';
server.listen((request) async {
final resolved = router.resolve(request.uri.toString(), (route) {
final resolved = router.resolveOnRoot(request.uri.toString(), filter: (route) {
print(
'$route matches ${request.method} ${request.uri}? ${route.method == request.method || route.method == '*'}');
return route.method == request.method || route.method == '*';

View file

@ -1 +0,0 @@
../../packages

View file

@ -22,14 +22,14 @@ main() {
group('no params', () {
test('resolve', () {
expect(child.resolve('a'), equals(a));
expect(child.resolve('b'), equals(b));
expect(child.resolve('b/c'), equals(c));
expect(child.resolveOnRoot('a'), equals(a));
expect(child.resolveOnRoot('b'), equals(b));
expect(child.resolveOnRoot('b/c'), equals(c));
expect(parent.resolve('child/a'), equals(a));
expect(parent.resolve('a'), isNull);
expect(parent.resolve('child/b'), equals(b));
expect(parent.resolve('child/b/c'), equals(c));
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 +0,0 @@
../../packages

View file

@ -1 +0,0 @@
../packages

View file

@ -1 +0,0 @@
../../packages

View file

@ -1 +0,0 @@
../../packages