Still need more browser tests, add server tests, fix kinks

This commit is contained in:
thosakwe 2016-10-19 18:04:06 -04:00
parent 2143d6c955
commit a9e4f6d4dc
11 changed files with 208 additions and 66 deletions

View file

@ -54,7 +54,7 @@ main() {
return someQuery(id).reviews.firstWhere(
(r) => r.id == reviewId);
});
}, before: [put, middleware, here]);
}, middleware: [put, middleware, here]);
}
```

View file

@ -1,3 +1,4 @@
/// A powerful, isomorphic routing library for Dart.
library angel_route;
export 'src/route.dart';

View file

@ -1,13 +1,16 @@
import 'dart:async' show Stream, StreamController;
import 'dart:convert' show JSON;
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)
@ -16,8 +19,13 @@ abstract class BrowserRouter extends Router {
BrowserRouter._([Route root]) : super(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();
}

View file

@ -8,7 +8,7 @@ final RegExp _rgxStraySlashes = new RegExp(r'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
String _matcherify(String path, {bool expand: true}) {
var p = path.replaceAll(new RegExp(r'\/\*$'), "*").replaceAll('/', r'\/');
var p = path.replaceAll(new RegExp(r'/\*$'), "*").replaceAll('/', r'\/');
if (expand) {
var match = _param.firstMatch(p);
@ -44,6 +44,7 @@ String _pathify(String path) {
return p;
}
/// Represents a virtual location within an application.
class Route {
final List<Route> _children = [];
final List _handlers = [];
@ -55,16 +56,36 @@ class Route {
String _path;
String _pathified;
RegExp _resolver;
String _stub;
RegExp _stub;
/// Set to `true` to print verbose debug output when interacting with this route.
bool debug;
/// Contains any child routes attached to this one.
List<Route> get children => new List.unmodifiable(_children);
/// A `List` of arbitrary objects chosen to respond to this request.
List get handlers => new List.unmodifiable(_handlers);
/// A `RegExp` that matches requests to this route.
RegExp get matcher => _matcher;
/// The HTTP method this route is designated for.
String get method => _method;
/// The name of this route, if any.
String get name => _name;
/// The hierarchical parent of this route.
Route get parent => _parent;
/// The virtual path on which this route is mounted.
String get path => _path;
/// Arbitrary state attached to this route.
final Extensible state = new Extensible();
/// The [Route] at the top of the hierarchy this route is found in.
Route get absoluteParent {
Route result = this;
@ -94,8 +115,13 @@ class Route {
return result;
}
void _printDebug(msg) {
if (debug) print(msg);
}
Route(Pattern path,
{Iterable<Route> children: const [],
this.debug: false,
Iterable handlers: const [],
method: "GET",
String name: null}) {
@ -134,7 +160,11 @@ class Route {
Iterable handlers: const [],
method: "GET",
String name: null}) {
final segments = path.toString().split('/').where((str) => str.isNotEmpty);
final segments = path
.toString()
.split('/')
.where((str) => str.isNotEmpty)
.toList(growable: false);
Route result;
if (segments.isEmpty) {
@ -142,11 +172,22 @@ class Route {
children: children, handlers: handlers, method: method, name: name);
}
for (final segment in segments) {
if (result == null) {
result = new Route(segment);
} else
result = result.child(segment);
for (int i = 0; i < segments.length; i++) {
final segment = segments[i];
if (i == segments.length - 1) {
if (result == null) {
result = new Route(segment);
} else {
result = result.child(segment);
}
} else {
if (result == null) {
result = new Route(segment, method: "*");
} else {
result = result.child(segment, method: "*");
}
}
}
result._children.addAll(children);
@ -157,6 +198,7 @@ class Route {
return result;
}
/// Combines the paths and matchers of two [Route] instances, and creates a new instance.
factory Route.join(Route parent, Route child) {
final String path1 = parent.path
.replaceAll(_rgxStart, '')
@ -183,15 +225,20 @@ class Route {
parent._children.add(route
.._matcher = new RegExp('$pattern1$separator$pattern2')
.._parent = parent);
.._parent = parent
.._stub = child.matcher);
parent._printDebug(
'Joined $path1 and $path2, produced stub ${route._stub.pattern}');
return route;
}
/// Calls [addChild] on all given routes.
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
return routes.map((route) => addChild(route, join: join)).toList();
}
/// Adds the given route as a hierarchical child of this one.
Route addChild(Route route, {bool join: true}) {
Route created = join ? new Route.join(this, route) : route.._parent = this;
return created;
@ -200,6 +247,7 @@ class Route {
/// Assigns a name to this route.
Route as(String name) => this.._name = name;
/// Creates a hierarchical child of this route with the given path.
Route child(Pattern path,
{Iterable<Route> children: const [],
Iterable handlers: const [],
@ -223,6 +271,7 @@ class Route {
return result.replaceAll("*", "");
}
/// Attempts to match a path against this route.
Match match(String path) =>
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
@ -256,17 +305,24 @@ class Route {
yield routeMatch.group(i);
}
/// Finds the first route available within this hierarchy that can respond to the given path.
///
/// Can be used to navigate a route hierarchy like a file system.
Route resolve(String path, {bool filter(Route route), String fullPath}) {
final _filter = filter ?? (_) => true;
final _fullPath = fullPath ?? path;
if ((path.isEmpty || path == '.') && _filter(this)) {
return this;
// Try to find index
_printDebug('INDEX???');
return children.firstWhere((r) => r.path.isEmpty, orElse: () => this);
} else if (path == '/') {
return absoluteParent.resolve('');
} else if (path.replaceAll(_straySlashes, '').isEmpty) {
for (Route route in children) {
final stub = route.path.replaceAll(this.path, '');
if (stub == '/' || stub.isEmpty && _filter(route)) return route;
if ((stub == '/' || stub.isEmpty) && _filter(route)) return route;
}
if (_filter(this))
@ -289,6 +345,11 @@ class Route {
return this;
} else {
final segments = path.split('/').where((str) => str.isNotEmpty).toList();
_printDebug('Segments: $segments on "/${this.path}"');
if (segments.isEmpty) {
return children.firstWhere((r) => r.path.isEmpty, orElse: () => this);
}
if (segments[0] == '..') {
if (parent != null)
@ -303,6 +364,8 @@ class Route {
for (Route route in children) {
final subPath = '${this.path}/${segments[0]}';
_printDebug(
'seg0: ${segments[0]}, stub: ${route._stub.pattern}, path: $path, route.path: ${route.path}, route.matcher: ${route.matcher.pattern}, this.matcher: ${matcher.pattern}');
if (route.match(subPath) != null ||
route._resolver.firstMatch(subPath) != null) {
@ -315,19 +378,22 @@ class Route {
'/' +
_fullPath.replaceAll(_straySlashes, ''));
}
} else if (route._stub != null && route._stub.hasMatch(segments[0])) {
_printDebug('MAYBE STUB?');
return route;
}
}
// Try to match "subdirectory"
for (Route route in children) {
print(
_printDebug(
'Trying to match subdir for $path; child ${route.path} on ${this.path}');
final match = route._parentResolver.firstMatch(path);
if (match != null) {
final subPath =
path.replaceFirst(match[0], '').replaceAll(_straySlashes, '');
print("Subdir path: $subPath");
_printDebug("Subdir path: $subPath");
for (Route child in route.children) {
final testPath = child.path
@ -341,15 +407,18 @@ class Route {
return child;
}
}
_printDebug('No subpath match: $subPath');
} else
print('Nope: $_parentResolver');
_printDebug('Nope: $_parentResolver');
}
/*
// Try to fill params
for (Route route in children) {
final params = parseParameters(_fullPath);
final _filledPath = makeUri(params);
print(
_printDebug(
'Trying to match filled $_filledPath for ${route.path} on ${this.path}');
if ((route.match(_filledPath) != null ||
route._resolver.firstMatch(_filledPath) != null) &&
@ -364,13 +433,13 @@ class Route {
_filter(route))
return route;
else {
print('Failed for ${route.matcher} when given $_filledPath');
_printDebug('Failed for ${route.matcher} when given $_filledPath');
}
}
}*/
// Try to match the whole route, if nothing else works
for (Route route in children) {
print(
_printDebug(
'Trying to match full $_fullPath for ${route.path} on ${this.path}');
if ((route.match(_fullPath) != null ||
route._resolver.firstMatch(_fullPath) != null) &&
@ -385,7 +454,7 @@ class Route {
_filter(route))
return route;
else {
print('Failed for ${route.matcher} when given $_fullPath');
_printDebug('Failed for ${route.matcher} when given $_fullPath');
}
}

View file

@ -1,17 +1,29 @@
import 'extensible.dart';
import 'route.dart';
import 'routing_exception.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
class Router extends Extensible {
/// Set to `true` to print verbose debug output when interacting with this route.
bool debug = false;
/// Additional filters to be run on designated requests.
Map<String, dynamic> requestMiddleware = {};
/// The single [Route] that serves as the root of the hierarchy.
final Route root;
/// Provide a `root` to make this Router revolve around a pre-defined route.
/// Not recommended.
Router([Route root]) : this.root = root ?? new Route('/', name: '<root>');
void _printDebug(msg) {
if (debug)
_printDebug(msg);
}
/// Adds a route that responds to the given path
/// for requests with the given method (case-insensitive).
/// Provide '*' as the method to respond to all methods.
@ -23,7 +35,62 @@ class Router extends Extensible {
..addAll(middleware ?? [])
..add(handler);
return root.child(path, handlers: handlers, method: method);
if (path is RegExp) {
return root.child(path, handlers: handlers, method: method);
} else if (path.toString().replaceAll(_straySlashes, '').isEmpty) {
return root.child(path.toString(), 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('/', handlers: handlers, method: method);
} else {
_printDebug('Want ${segments[0]}');
result = resolve(segments[0]);
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]);
if (existing != null) {
result = existing;
}
} while (existing != null);
} else throw new RoutingException("Cannot overwrite existing route '${segments[0]}'.");
}
}
for (int i = 0; i < segments.length; i++) {
final segment = segments[i];
if (i == segments.length - 1) {
if (result == null) {
result = root.child(segment, handlers: handlers, method: method);
} else {
result = result.child(segment, handlers: handlers, method: method);
}
} else {
if (result == null) {
result = root.child(segment, method: "*");
} else {
result = result.child(segment, method: "*");
}
}
}
return result;
}
}
/// Creates a visual representation of the route hierarchy and
@ -45,10 +112,8 @@ class Router extends Extensible {
else
buf.write("'${p.replaceAll(_straySlashes, '')}'");
buf.write(' => ');
if (route.handlers.isNotEmpty)
buf.writeln('${route.handlers.length} handler(s)');
buf.writeln(' => ${route.handlers.length} handler(s)');
else
buf.writeln();
@ -60,7 +125,7 @@ class Router extends Extensible {
if (header != null && header.isNotEmpty) buf.writeln(header);
dumpRoute(root);
(callback ?? print)(buf);
(callback ?? print)(buf.toString());
}
/// Creates a route, and allows you to add child routes to it

View file

@ -1,6 +1,11 @@
/// Represents an error in route configuration or navigation.
abstract class RoutingException extends Exception {
factory RoutingException(String message) => new _RoutingExceptionImpl(message);
/// Occurs when trying to resolve the parent of a [Route] without a parent.
factory RoutingException.orphan() => new _RoutingExceptionImpl("Tried to resolve path '..' on a route that has no parent.");
/// Occurs when the user attempts to navigate to a non-existent route.
factory RoutingException.noSuchRoute(String path) => new _RoutingExceptionImpl("Tried to navigate to non-existent route: '$path'.");
}

View file

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

View file

@ -2,55 +2,55 @@ import 'package:angel_route/angel_route.dart';
import 'package:test/test.dart';
main() {
final fooById = new Route.build('/foo/:id([0-9]+)', handlers: ['bar']);
final foo = fooById.parent;
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');
final bazById = baz.child(':bazId([A-Za-z]+)');
new Router(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('foo/:id'));
expect(fooById.match('/foo/2'), isNotNull);
expect(fooById.match('/foo/aaa'), isNull);
expect(fooById.path, equals(':id'));
expect(fooById.match('/2'), isNotNull);
expect(fooById.match('/aaa'), isNull);
expect(fooById.match('/bar'), isNull);
expect(fooById.match('/foolish'), isNull);
expect(fooById.match('lish'), isNull);
expect(fooById.parent, equals(foo));
expect(fooById.absoluteParent, equals(foo));
expect(bar.path, equals('foo/:id/bar'));
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('/foo/2/bar'), isNotNull);
expect(bar.match('/2/bar'), isNotNull);
expect(bar.match('/bar'), isNull);
expect(bar.match('/foo/a/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('foo/:id/bar/baz'));
expect(baz.match('/foo/2A/bar/baz'), isNull);
expect(baz.match('/foo/2/bar/baz'), isNotNull);
expect(baz.match('/foo/1337/bar/baz'), isNotNull);
expect(baz.match('/foo/bat/baz'), isNull);
expect(baz.match('/foo/bar/baz/1'), isNull);
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('/foo/2'), equals(fooById));
expect(fooById.resolve('/2'), equals(fooById));
expect(fooById.resolve('/foo/2/bar'), equals(bar));
expect(fooById.resolve('/foo/bar'), isNull);
expect(fooById.resolve('/foo/a/bar'), isNull);
expect(fooById.resolve('foo/1337/bar/baz'), equals(baz));
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));
@ -67,11 +67,10 @@ main() {
expect(baz.resolve('/2/bar'), equals(bar));
expect(baz.resolve('/1337/bar/baz'), equals(baz));
expect(bar.resolve('/2/baz/e'), equals(bazById));
expect(bar.resolve('/2/bar/baz/e'), equals(bazById));
expect(bar.resolve('baz/e'), equals(bazById));
expect(bar.resolve('baz/e'), isNull);
expect(fooById.resolve('/foo/2/baz/e'), equals(bazById));
expect(fooById.resolve('/foo/2/baz/2'), isNull);
expect(fooById.resolve('/foo/2a/baz/e'), isNull);
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

@ -7,7 +7,8 @@ final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
main() {
final router = new Router();
final indexRoute = router.get('/', () => ':)');
final userById = router.delete('/user/:id/detail', (id) => num.parse(id));
final fizz = router.post('/user/fizz', null);
final deleteUserById = router.delete('/user/:id/detail', (id) => num.parse(id));
Route lower;
final letters = router.group('/letter///', (router) {
@ -19,9 +20,7 @@ main() {
.child('/upper', handlers: [(String id) => id.toUpperCase()[0]]);
});
final fizz = router.post('/user/fizz', null);
router.dumpTree();
router.dumpTree(header: "ROUTER TESTS");
test('extensible', () {
router['two'] = 2;
@ -34,14 +33,14 @@ main() {
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(userById));
expect(userById.resolve('../fizz'), equals(fizz));
expect(lower.resolve('/user/34/detail'), equals(deleteUserById));
expect(deleteUserById.resolve('../../fizz'), equals(fizz));
});
test('resolve', () {
expect(router.resolve('/'), equals(indexRoute));
expect(router.resolve('user/1337/detail'), equals(userById));
expect(router.resolve('/user/1337/detail'), equals(userById));
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);
});

View file

@ -1,5 +0,0 @@
main() {
final uri = Uri.parse('/foo');
print(uri);
print(uri.resolve('/bar'));
}

View file

@ -23,6 +23,7 @@ basic(BrowserRouter router) {
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']);