Still need more browser tests, add server tests, fix kinks
This commit is contained in:
parent
2143d6c955
commit
a9e4f6d4dc
11 changed files with 208 additions and 66 deletions
|
@ -54,7 +54,7 @@ main() {
|
||||||
return someQuery(id).reviews.firstWhere(
|
return someQuery(id).reviews.firstWhere(
|
||||||
(r) => r.id == reviewId);
|
(r) => r.id == reviewId);
|
||||||
});
|
});
|
||||||
}, before: [put, middleware, here]);
|
}, middleware: [put, middleware, here]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/// A powerful, isomorphic routing library for Dart.
|
||||||
library angel_route;
|
library angel_route;
|
||||||
|
|
||||||
export 'src/route.dart';
|
export 'src/route.dart';
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import 'dart:async' show Stream, StreamController;
|
import 'dart:async' show Stream, StreamController;
|
||||||
import 'dart:convert' show JSON;
|
|
||||||
import 'dart:html' show AnchorElement, window;
|
import 'dart:html' show AnchorElement, window;
|
||||||
import 'angel_route.dart';
|
import 'angel_route.dart';
|
||||||
|
|
||||||
final RegExp _hash = new RegExp(r'^#/');
|
final RegExp _hash = new RegExp(r'^#/');
|
||||||
|
|
||||||
|
/// A variation of the [Router] support both hash routing and push state.
|
||||||
abstract class BrowserRouter extends Router {
|
abstract class BrowserRouter extends Router {
|
||||||
|
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
||||||
Stream<Route> get onRoute;
|
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}) {
|
factory BrowserRouter({bool hash: false, bool listen: true, Route root}) {
|
||||||
return hash
|
return hash
|
||||||
? new _HashRouter(listen: listen, root: root)
|
? new _HashRouter(listen: listen, root: root)
|
||||||
|
@ -16,8 +19,13 @@ abstract class BrowserRouter extends Router {
|
||||||
|
|
||||||
BrowserRouter._([Route root]) : super(root);
|
BrowserRouter._([Route root]) : super(root);
|
||||||
|
|
||||||
|
/// Calls `goTo` on the [Route] matching `path`.
|
||||||
void go(String path, [Map params]);
|
void go(String path, [Map params]);
|
||||||
|
|
||||||
|
/// Navigates to the given route.
|
||||||
void goTo(Route route, [Map params]);
|
void goTo(Route route, [Map params]);
|
||||||
|
|
||||||
|
/// Begins listen for location changes.
|
||||||
void listen();
|
void listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ final RegExp _rgxStraySlashes = new RegExp(r'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
|
||||||
String _matcherify(String path, {bool expand: true}) {
|
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) {
|
if (expand) {
|
||||||
var match = _param.firstMatch(p);
|
var match = _param.firstMatch(p);
|
||||||
|
@ -44,6 +44,7 @@ String _pathify(String path) {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a virtual location within an application.
|
||||||
class Route {
|
class Route {
|
||||||
final List<Route> _children = [];
|
final List<Route> _children = [];
|
||||||
final List _handlers = [];
|
final List _handlers = [];
|
||||||
|
@ -55,16 +56,36 @@ class Route {
|
||||||
String _path;
|
String _path;
|
||||||
String _pathified;
|
String _pathified;
|
||||||
RegExp _resolver;
|
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);
|
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);
|
List get handlers => new List.unmodifiable(_handlers);
|
||||||
|
|
||||||
|
/// A `RegExp` that matches requests to this route.
|
||||||
RegExp get matcher => _matcher;
|
RegExp get matcher => _matcher;
|
||||||
|
|
||||||
|
/// The HTTP method this route is designated for.
|
||||||
String get method => _method;
|
String get method => _method;
|
||||||
|
|
||||||
|
/// The name of this route, if any.
|
||||||
String get name => _name;
|
String get name => _name;
|
||||||
|
|
||||||
|
/// The hierarchical parent of this route.
|
||||||
Route get parent => _parent;
|
Route get parent => _parent;
|
||||||
|
|
||||||
|
/// The virtual path on which this route is mounted.
|
||||||
String get path => _path;
|
String get path => _path;
|
||||||
|
|
||||||
|
/// Arbitrary state attached to this route.
|
||||||
final Extensible state = new Extensible();
|
final Extensible state = new Extensible();
|
||||||
|
|
||||||
|
/// The [Route] at the top of the hierarchy this route is found in.
|
||||||
Route get absoluteParent {
|
Route get absoluteParent {
|
||||||
Route result = this;
|
Route result = this;
|
||||||
|
|
||||||
|
@ -94,8 +115,13 @@ class Route {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _printDebug(msg) {
|
||||||
|
if (debug) print(msg);
|
||||||
|
}
|
||||||
|
|
||||||
Route(Pattern path,
|
Route(Pattern path,
|
||||||
{Iterable<Route> children: const [],
|
{Iterable<Route> children: const [],
|
||||||
|
this.debug: false,
|
||||||
Iterable handlers: const [],
|
Iterable handlers: const [],
|
||||||
method: "GET",
|
method: "GET",
|
||||||
String name: null}) {
|
String name: null}) {
|
||||||
|
@ -134,7 +160,11 @@ class Route {
|
||||||
Iterable handlers: const [],
|
Iterable handlers: const [],
|
||||||
method: "GET",
|
method: "GET",
|
||||||
String name: null}) {
|
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;
|
Route result;
|
||||||
|
|
||||||
if (segments.isEmpty) {
|
if (segments.isEmpty) {
|
||||||
|
@ -142,11 +172,22 @@ class Route {
|
||||||
children: children, handlers: handlers, method: method, name: name);
|
children: children, handlers: handlers, method: method, name: name);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final segment in segments) {
|
for (int i = 0; i < segments.length; i++) {
|
||||||
if (result == null) {
|
final segment = segments[i];
|
||||||
result = new Route(segment);
|
|
||||||
} else
|
if (i == segments.length - 1) {
|
||||||
result = result.child(segment);
|
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);
|
result._children.addAll(children);
|
||||||
|
@ -157,6 +198,7 @@ class Route {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Combines the paths and matchers of two [Route] instances, and creates a new instance.
|
||||||
factory Route.join(Route parent, Route child) {
|
factory Route.join(Route parent, Route child) {
|
||||||
final String path1 = parent.path
|
final String path1 = parent.path
|
||||||
.replaceAll(_rgxStart, '')
|
.replaceAll(_rgxStart, '')
|
||||||
|
@ -183,15 +225,20 @@ class Route {
|
||||||
|
|
||||||
parent._children.add(route
|
parent._children.add(route
|
||||||
.._matcher = new RegExp('$pattern1$separator$pattern2')
|
.._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;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls [addChild] on all given routes.
|
||||||
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
|
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
|
||||||
return routes.map((route) => addChild(route, join: join)).toList();
|
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 addChild(Route route, {bool join: true}) {
|
||||||
Route created = join ? new Route.join(this, route) : route.._parent = this;
|
Route created = join ? new Route.join(this, route) : route.._parent = this;
|
||||||
return created;
|
return created;
|
||||||
|
@ -200,6 +247,7 @@ class Route {
|
||||||
/// Assigns a name to this route.
|
/// Assigns a name to this route.
|
||||||
Route as(String name) => this.._name = name;
|
Route as(String name) => this.._name = name;
|
||||||
|
|
||||||
|
/// Creates a hierarchical child of this route with the given path.
|
||||||
Route child(Pattern path,
|
Route child(Pattern path,
|
||||||
{Iterable<Route> children: const [],
|
{Iterable<Route> children: const [],
|
||||||
Iterable handlers: const [],
|
Iterable handlers: const [],
|
||||||
|
@ -223,6 +271,7 @@ class Route {
|
||||||
return result.replaceAll("*", "");
|
return result.replaceAll("*", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to match a path against this route.
|
||||||
Match match(String path) =>
|
Match match(String path) =>
|
||||||
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
|
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
|
||||||
|
|
||||||
|
@ -256,17 +305,24 @@ class Route {
|
||||||
yield routeMatch.group(i);
|
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}) {
|
Route resolve(String path, {bool filter(Route route), String fullPath}) {
|
||||||
final _filter = filter ?? (_) => true;
|
final _filter = filter ?? (_) => true;
|
||||||
final _fullPath = fullPath ?? path;
|
final _fullPath = fullPath ?? path;
|
||||||
|
|
||||||
if ((path.isEmpty || path == '.') && _filter(this)) {
|
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) {
|
} else if (path.replaceAll(_straySlashes, '').isEmpty) {
|
||||||
for (Route route in children) {
|
for (Route route in children) {
|
||||||
final stub = route.path.replaceAll(this.path, '');
|
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))
|
if (_filter(this))
|
||||||
|
@ -289,6 +345,11 @@ class Route {
|
||||||
return this;
|
return this;
|
||||||
} else {
|
} else {
|
||||||
final segments = path.split('/').where((str) => str.isNotEmpty).toList();
|
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 (segments[0] == '..') {
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
|
@ -303,6 +364,8 @@ class Route {
|
||||||
|
|
||||||
for (Route route in children) {
|
for (Route route in children) {
|
||||||
final subPath = '${this.path}/${segments[0]}';
|
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 ||
|
if (route.match(subPath) != null ||
|
||||||
route._resolver.firstMatch(subPath) != null) {
|
route._resolver.firstMatch(subPath) != null) {
|
||||||
|
@ -315,19 +378,22 @@ class Route {
|
||||||
'/' +
|
'/' +
|
||||||
_fullPath.replaceAll(_straySlashes, ''));
|
_fullPath.replaceAll(_straySlashes, ''));
|
||||||
}
|
}
|
||||||
|
} else if (route._stub != null && route._stub.hasMatch(segments[0])) {
|
||||||
|
_printDebug('MAYBE STUB?');
|
||||||
|
return route;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to match "subdirectory"
|
// Try to match "subdirectory"
|
||||||
for (Route route in children) {
|
for (Route route in children) {
|
||||||
print(
|
_printDebug(
|
||||||
'Trying to match subdir for $path; child ${route.path} on ${this.path}');
|
'Trying to match subdir for $path; child ${route.path} on ${this.path}');
|
||||||
final match = route._parentResolver.firstMatch(path);
|
final match = route._parentResolver.firstMatch(path);
|
||||||
|
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
final subPath =
|
final subPath =
|
||||||
path.replaceFirst(match[0], '').replaceAll(_straySlashes, '');
|
path.replaceFirst(match[0], '').replaceAll(_straySlashes, '');
|
||||||
print("Subdir path: $subPath");
|
_printDebug("Subdir path: $subPath");
|
||||||
|
|
||||||
for (Route child in route.children) {
|
for (Route child in route.children) {
|
||||||
final testPath = child.path
|
final testPath = child.path
|
||||||
|
@ -341,15 +407,18 @@ class Route {
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_printDebug('No subpath match: $subPath');
|
||||||
} else
|
} else
|
||||||
print('Nope: $_parentResolver');
|
_printDebug('Nope: $_parentResolver');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// Try to fill params
|
// Try to fill params
|
||||||
for (Route route in children) {
|
for (Route route in children) {
|
||||||
final params = parseParameters(_fullPath);
|
final params = parseParameters(_fullPath);
|
||||||
final _filledPath = makeUri(params);
|
final _filledPath = makeUri(params);
|
||||||
print(
|
_printDebug(
|
||||||
'Trying to match filled $_filledPath for ${route.path} on ${this.path}');
|
'Trying to match filled $_filledPath for ${route.path} on ${this.path}');
|
||||||
if ((route.match(_filledPath) != null ||
|
if ((route.match(_filledPath) != null ||
|
||||||
route._resolver.firstMatch(_filledPath) != null) &&
|
route._resolver.firstMatch(_filledPath) != null) &&
|
||||||
|
@ -364,13 +433,13 @@ class Route {
|
||||||
_filter(route))
|
_filter(route))
|
||||||
return route;
|
return route;
|
||||||
else {
|
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
|
// Try to match the whole route, if nothing else works
|
||||||
for (Route route in children) {
|
for (Route route in children) {
|
||||||
print(
|
_printDebug(
|
||||||
'Trying to match full $_fullPath for ${route.path} on ${this.path}');
|
'Trying to match full $_fullPath for ${route.path} on ${this.path}');
|
||||||
if ((route.match(_fullPath) != null ||
|
if ((route.match(_fullPath) != null ||
|
||||||
route._resolver.firstMatch(_fullPath) != null) &&
|
route._resolver.firstMatch(_fullPath) != null) &&
|
||||||
|
@ -385,7 +454,7 @@ class Route {
|
||||||
_filter(route))
|
_filter(route))
|
||||||
return route;
|
return route;
|
||||||
else {
|
else {
|
||||||
print('Failed for ${route.matcher} when given $_fullPath');
|
_printDebug('Failed for ${route.matcher} when given $_fullPath');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
import 'extensible.dart';
|
import 'extensible.dart';
|
||||||
import 'route.dart';
|
import 'route.dart';
|
||||||
|
import 'routing_exception.dart';
|
||||||
|
|
||||||
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. :)
|
||||||
class Router extends Extensible {
|
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.
|
/// 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.
|
/// The single [Route] that serves as the root of the hierarchy.
|
||||||
final Route root;
|
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>');
|
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
|
/// Adds a route that responds to the given path
|
||||||
/// 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.
|
||||||
|
@ -23,7 +35,62 @@ class Router extends Extensible {
|
||||||
..addAll(middleware ?? [])
|
..addAll(middleware ?? [])
|
||||||
..add(handler);
|
..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
|
/// Creates a visual representation of the route hierarchy and
|
||||||
|
@ -45,10 +112,8 @@ class Router extends Extensible {
|
||||||
else
|
else
|
||||||
buf.write("'${p.replaceAll(_straySlashes, '')}'");
|
buf.write("'${p.replaceAll(_straySlashes, '')}'");
|
||||||
|
|
||||||
buf.write(' => ');
|
|
||||||
|
|
||||||
if (route.handlers.isNotEmpty)
|
if (route.handlers.isNotEmpty)
|
||||||
buf.writeln('${route.handlers.length} handler(s)');
|
buf.writeln(' => ${route.handlers.length} handler(s)');
|
||||||
else
|
else
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
|
|
||||||
|
@ -60,7 +125,7 @@ class Router extends Extensible {
|
||||||
if (header != null && header.isNotEmpty) buf.writeln(header);
|
if (header != null && header.isNotEmpty) buf.writeln(header);
|
||||||
|
|
||||||
dumpRoute(root);
|
dumpRoute(root);
|
||||||
(callback ?? print)(buf);
|
(callback ?? print)(buf.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a route, and allows you to add child routes to it
|
/// Creates a route, and allows you to add child routes to it
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
/// Represents an error in route configuration or navigation.
|
||||||
abstract class RoutingException extends Exception {
|
abstract class RoutingException extends Exception {
|
||||||
factory RoutingException(String message) => new _RoutingExceptionImpl(message);
|
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.");
|
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'.");
|
factory RoutingException.noSuchRoute(String path) => new _RoutingExceptionImpl("Tried to navigate to non-existent route: '$path'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: angel_route
|
name: angel_route
|
||||||
description: A powerful, isomorphic routing library for Dart.
|
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>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_route
|
homepage: https://github.com/angel-dart/angel_route
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -2,55 +2,55 @@ import 'package:angel_route/angel_route.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
final fooById = new Route.build('/foo/:id([0-9]+)', handlers: ['bar']);
|
final foo = new Router().root;
|
||||||
final foo = fooById.parent;
|
final fooById = foo.child(':id((?!bar)[0-9]+)', handlers: ['bar']);
|
||||||
final bar = fooById.child('bar');
|
final bar = fooById.child('bar');
|
||||||
final baz = bar.child('//////baz//////', handlers: ['hello', 'world']);
|
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();
|
new Router(foo).dumpTree();
|
||||||
|
|
||||||
test('matching', () {
|
test('matching', () {
|
||||||
expect(fooById.children.length, equals(1));
|
expect(fooById.children.length, equals(1));
|
||||||
expect(fooById.handlers.length, equals(1));
|
expect(fooById.handlers.length, equals(1));
|
||||||
expect(fooById.handlerSequence.length, equals(1));
|
expect(fooById.handlerSequence.length, equals(1));
|
||||||
expect(fooById.path, equals('foo/:id'));
|
expect(fooById.path, equals(':id'));
|
||||||
expect(fooById.match('/foo/2'), isNotNull);
|
expect(fooById.match('/2'), isNotNull);
|
||||||
expect(fooById.match('/foo/aaa'), isNull);
|
expect(fooById.match('/aaa'), isNull);
|
||||||
expect(fooById.match('/bar'), isNull);
|
expect(fooById.match('/bar'), isNull);
|
||||||
expect(fooById.match('/foolish'), isNull);
|
expect(fooById.match('lish'), isNull);
|
||||||
expect(fooById.parent, equals(foo));
|
expect(fooById.parent, equals(foo));
|
||||||
expect(fooById.absoluteParent, 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.children.length, equals(1));
|
||||||
expect(bar.handlers, isEmpty);
|
expect(bar.handlers, isEmpty);
|
||||||
expect(bar.handlerSequence.length, equals(1));
|
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('/bar'), isNull);
|
||||||
expect(bar.match('/foo/a/bar'), isNull);
|
expect(bar.match('/a/bar'), isNull);
|
||||||
expect(bar.parent, equals(fooById));
|
expect(bar.parent, equals(fooById));
|
||||||
expect(baz.absoluteParent, equals(foo));
|
expect(baz.absoluteParent, equals(foo));
|
||||||
|
|
||||||
expect(baz.children.length, equals(1));
|
expect(baz.children.length, equals(1));
|
||||||
expect(baz.handlers.length, equals(2));
|
expect(baz.handlers.length, equals(2));
|
||||||
expect(baz.handlerSequence.length, equals(3));
|
expect(baz.handlerSequence.length, equals(3));
|
||||||
expect(baz.path, equals('foo/:id/bar/baz'));
|
expect(baz.path, equals(':id/bar/baz'));
|
||||||
expect(baz.match('/foo/2A/bar/baz'), isNull);
|
expect(baz.match('/2A/bar/baz'), isNull);
|
||||||
expect(baz.match('/foo/2/bar/baz'), isNotNull);
|
expect(baz.match('/2/bar/baz'), isNotNull);
|
||||||
expect(baz.match('/foo/1337/bar/baz'), isNotNull);
|
expect(baz.match('/1337/bar/baz'), isNotNull);
|
||||||
expect(baz.match('/foo/bat/baz'), isNull);
|
expect(baz.match('/bat/baz'), isNull);
|
||||||
expect(baz.match('/foo/bar/baz/1'), isNull);
|
expect(baz.match('/bar/baz/1'), isNull);
|
||||||
expect(baz.parent, equals(bar));
|
expect(baz.parent, equals(bar));
|
||||||
expect(baz.absoluteParent, equals(foo));
|
expect(baz.absoluteParent, equals(foo));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hierarchy', () {
|
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('/2/bar'), equals(bar));
|
||||||
expect(fooById.resolve('/foo/bar'), isNull);
|
expect(fooById.resolve('/bar'), isNull);
|
||||||
expect(fooById.resolve('/foo/a/bar'), isNull);
|
expect(fooById.resolve('/a/bar'), isNull);
|
||||||
expect(fooById.resolve('foo/1337/bar/baz'), equals(baz));
|
expect(fooById.resolve('1337/bar/baz'), equals(baz));
|
||||||
|
|
||||||
expect(bar.resolve('..'), equals(fooById));
|
expect(bar.resolve('..'), equals(fooById));
|
||||||
|
|
||||||
|
@ -67,11 +67,10 @@ main() {
|
||||||
expect(baz.resolve('/2/bar'), equals(bar));
|
expect(baz.resolve('/2/bar'), equals(bar));
|
||||||
expect(baz.resolve('/1337/bar/baz'), equals(baz));
|
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'), equals(bazById));
|
||||||
expect(bar.resolve('baz/e'), isNull);
|
expect(fooById.resolve('/2/bar/baz/e'), equals(bazById));
|
||||||
expect(fooById.resolve('/foo/2/baz/e'), equals(bazById));
|
expect(fooById.resolve('/2/bar/baz/2'), isNull);
|
||||||
expect(fooById.resolve('/foo/2/baz/2'), isNull);
|
expect(fooById.resolve('/2a/bar/baz/e'), isNull);
|
||||||
expect(fooById.resolve('/foo/2a/baz/e'), isNull);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||||
main() {
|
main() {
|
||||||
final router = new Router();
|
final router = new Router();
|
||||||
final indexRoute = router.get('/', () => ':)');
|
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;
|
Route lower;
|
||||||
final letters = router.group('/letter///', (router) {
|
final letters = router.group('/letter///', (router) {
|
||||||
|
@ -19,9 +20,7 @@ main() {
|
||||||
.child('/upper', handlers: [(String id) => id.toUpperCase()[0]]);
|
.child('/upper', handlers: [(String id) => id.toUpperCase()[0]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
final fizz = router.post('/user/fizz', null);
|
router.dumpTree(header: "ROUTER TESTS");
|
||||||
|
|
||||||
router.dumpTree();
|
|
||||||
|
|
||||||
test('extensible', () {
|
test('extensible', () {
|
||||||
router['two'] = 2;
|
router['two'] = 2;
|
||||||
|
@ -34,14 +33,14 @@ main() {
|
||||||
expect(lower.absoluteParent, equals(router.root));
|
expect(lower.absoluteParent, equals(router.root));
|
||||||
expect(lower.parent.path, equals('letter/:id'));
|
expect(lower.parent.path, equals('letter/:id'));
|
||||||
expect(lower.resolve('../upper').path, equals('letter/:id/upper'));
|
expect(lower.resolve('../upper').path, equals('letter/:id/upper'));
|
||||||
expect(lower.resolve('/user/34/detail'), equals(userById));
|
expect(lower.resolve('/user/34/detail'), equals(deleteUserById));
|
||||||
expect(userById.resolve('../fizz'), equals(fizz));
|
expect(deleteUserById.resolve('../../fizz'), equals(fizz));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolve', () {
|
test('resolve', () {
|
||||||
expect(router.resolve('/'), equals(indexRoute));
|
expect(router.resolve('/'), equals(indexRoute));
|
||||||
expect(router.resolve('user/1337/detail'), equals(userById));
|
expect(router.resolve('user/1337/detail'), equals(deleteUserById));
|
||||||
expect(router.resolve('/user/1337/detail'), equals(userById));
|
expect(router.resolve('/user/1337/detail'), equals(deleteUserById));
|
||||||
expect(router.resolve('letter/a/lower'), equals(lower));
|
expect(router.resolve('letter/a/lower'), equals(lower));
|
||||||
expect(router.resolve('letter/2/lower'), isNull);
|
expect(router.resolve('letter/2/lower'), isNull);
|
||||||
});
|
});
|
||||||
|
|
5
uri.dart
5
uri.dart
|
@ -1,5 +0,0 @@
|
||||||
main() {
|
|
||||||
final uri = Uri.parse('/foo');
|
|
||||||
print(uri);
|
|
||||||
print(uri.resolve('/bar'));
|
|
||||||
}
|
|
|
@ -23,6 +23,7 @@ basic(BrowserRouter router) {
|
||||||
router.get('a', 'a handler');
|
router.get('a', 'a handler');
|
||||||
|
|
||||||
router.group('b', (router) {
|
router.group('b', (router) {
|
||||||
|
print(router.root);
|
||||||
router.get('a', 'b/a handler');
|
router.get('a', 'b/a handler');
|
||||||
router.get('b', 'b/b handler', middleware: ['b/b middleware']);
|
router.get('b', 'b/b handler', middleware: ['b/b middleware']);
|
||||||
}, middleware: ['b middleware']);
|
}, middleware: ['b middleware']);
|
||||||
|
|
Loading…
Reference in a new issue