platform/lib/src/router.dart

448 lines
14 KiB
Dart
Raw Normal View History

2016-11-22 03:31:09 +00:00
library angel_route.src.router;
2016-10-12 17:58:32 +00:00
import 'extensible.dart';
import 'routing_exception.dart';
2016-10-12 17:58:32 +00:00
2016-11-22 03:31:09 +00:00
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'/+\$');
2016-10-12 17:58:32 +00:00
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
2016-10-12 17:58:32 +00:00
class Router extends Extensible {
2016-10-21 03:13:13 +00:00
Route _root;
/// Set to `true` to print verbose debug output when interacting with this route.
bool debug = false;
2016-10-12 17:58:32 +00:00
/// Additional filters to be run on designated requests.
Map<String, dynamic> requestMiddleware = {};
/// The single [Route] that serves as the root of the hierarchy.
2016-10-21 03:13:13 +00:00
Route get root => _root;
2016-10-12 17:58:32 +00:00
/// Provide a `root` to make this Router revolve around a pre-defined route.
/// Not recommended.
2016-10-21 03:13:13 +00:00
Router({this.debug: false, Route root}) {
2016-11-22 03:31:09 +00:00
_root = (_root = root ?? new _RootRoute())
..debug = debug;
2016-10-21 03:13:13 +00:00
}
2016-10-12 17:58:32 +00:00
void _printDebug(msg) {
2016-10-21 03:13:13 +00:00
if (debug) print(msg);
}
2016-10-12 17:58:32 +00:00
/// 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.
Route addRoute(String method, Pattern path, Object handler,
{List middleware}) {
List handlers = [];
handlers
..addAll(middleware ?? [])
..add(handler);
if (path is RegExp) {
2016-10-21 03:13:13 +00:00
return root.child(path, debug: debug, handlers: handlers, method: method);
2016-11-22 03:31:09 +00:00
} else if (path
.toString()
.replaceAll(_straySlashes, '')
.isEmpty) {
2016-10-21 03:13:13 +00:00
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) {
2016-10-21 03:13:13 +00:00
return new Route('/', debug: debug, handlers: handlers, method: method)
..debug = debug;
} else {
2016-11-22 03:31:09 +00:00
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 {
2016-10-23 00:52:28 +00:00
existing = result.resolve(segments[0],
filter: (route) =>
2016-11-22 03:31:09 +00:00
route.method == method || route.method == '*');
if (existing != null) {
result = existing;
}
} while (existing != null);
2016-10-22 18:46:16 +00:00
}
}
}
for (int i = 0; i < segments.length; i++) {
final segment = segments[i];
if (i == segments.length - 1) {
if (result == null) {
2016-10-21 03:13:13 +00:00
result = root.child(segment,
debug: debug, handlers: handlers, method: method);
} else {
2016-10-21 03:13:13 +00:00
result = result.child(segment,
debug: debug, handlers: handlers, method: method);
}
} else {
if (result == null) {
2016-10-21 03:13:13 +00:00
result = root.child(segment, debug: debug, method: "*");
} else {
2016-10-21 03:13:13 +00:00
result = result.child(segment, debug: debug, method: "*");
}
}
}
2016-10-20 09:21:59 +00:00
return result..debug = debug;
}
2016-10-12 17:58:32 +00:00
}
/// Creates a visual representation of the route hierarchy and
/// passes it to a callback. If none is provided, `print` is called.
void dumpTree(
{callback(String tree), header: 'Dumping route tree:', tab: ' '}) {
var tabs = 0;
final buf = new StringBuffer();
2016-10-23 00:52:28 +00:00
void dumpRoute(Route route, {Pattern replace: null}) {
2016-11-22 03:31:09 +00:00
for (var i = 0; i < tabs; i++)
buf.write(tab);
2016-10-20 09:21:59 +00:00
if (route == root)
2016-11-22 03:31:09 +00:00
buf.writeln('(root)');
2016-10-23 00:52:28 +00:00
else {
2016-10-21 03:13:13 +00:00
buf.write('- ${route.method} ');
2016-10-12 17:58:32 +00:00
2016-11-22 03:31:09 +00:00
var p =
replace != null ? route.path.replaceAll(replace, '') : route.path;
p = p.replaceAll(_straySlashes, '');
2016-10-12 17:58:32 +00:00
2016-10-23 00:52:28 +00:00
if (p.isEmpty)
buf.write("'/'");
else
buf.write("'${p.replaceAll(_straySlashes, '')}'");
2016-10-12 17:58:32 +00:00
2016-10-23 00:52:28 +00:00
if (route.handlers.isNotEmpty)
buf.writeln(' => ${route.handlers.length} handler(s)');
else
buf.writeln();
}
2016-10-12 17:58:32 +00:00
tabs++;
2016-10-23 00:52:28 +00:00
route.children
.forEach((r) => dumpRoute(r, replace: new RegExp("^${route.path}")));
2016-10-12 17:58:32 +00:00
tabs--;
}
if (header != null && header.isNotEmpty) buf.writeln(header);
dumpRoute(root);
(callback ?? print)(buf.toString());
2016-10-12 17:58:32 +00:00
}
/// Creates a route, and allows you to add child routes to it
/// via a [Router] instance.
///
/// Returns the created route.
/// You can also register middleware within the router.
Route group(Pattern path, void callback(Router router),
{Iterable middleware: const [],
String method: "*",
String name: null,
String namespace: null}) {
final route =
2016-11-22 03:31:09 +00:00
root.child(path, handlers: middleware, method: method, name: name);
2016-10-21 03:13:13 +00:00
final router = new Router(root: route);
2016-10-12 17:58:32 +00:00
callback(router);
// Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = namespace != null ? "$namespace." : "";
Map copiedMiddleware = new Map.from(router.requestMiddleware);
for (String middlewareName in copiedMiddleware.keys) {
requestMiddleware["$middlewarePrefix$middlewareName"] =
2016-11-22 03:31:09 +00:00
copiedMiddleware[middlewareName];
2016-10-12 17:58:32 +00:00
}
return route;
}
/// Assigns a middleware to a name for convenience.
registerMiddleware(String 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.
2016-11-22 03:31:09 +00:00
Route resolveOnRoot(String path, {bool filter(Route route)}) =>
root.resolve(path, filter: filter);
2016-10-12 17:58:32 +00:00
2016-11-22 03:31:09 +00:00
/// 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;
}
2016-10-12 17:58:32 +00:00
/// Incorporates another [Router]'s routes into this one's.
///
/// If `hooked` is set to `true` and a [Service] is provided,
/// then that service will be wired to a [HookedService] proxy.
/// If a `namespace` is provided, then any middleware
/// from the provided [Router] will be prefixed by that namespace,
/// with a dot.
/// For example, if the [Router] has a middleware 'y', and the `namespace`
/// is 'x', then that middleware will be available as 'x.y' in the main router.
/// These namespaces can be nested.
2016-10-22 15:21:04 +00:00
void mount(Pattern path, Router router,
2016-10-12 17:58:32 +00:00
{bool hooked: true, String namespace: null}) {
// Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = namespace != null ? "$namespace." : "";
Map copiedMiddleware = new Map.from(router.requestMiddleware);
for (String middlewareName in copiedMiddleware.keys) {
requestMiddleware["$middlewarePrefix$middlewareName"] =
2016-11-22 03:31:09 +00:00
copiedMiddleware[middlewareName];
}
// 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);
}
2016-10-12 17:58:32 +00:00
}
2016-11-22 03:31:09 +00:00
root.children.forEach(_normalize);
2016-10-12 17:58:32 +00:00
}
/// Adds a route that responds to any request matching the given path.
Route all(Pattern path, Object handler, {List middleware}) {
return addRoute('*', path, handler, middleware: middleware);
}
/// Adds a route that responds to a DELETE request.
Route delete(Pattern path, Object handler, {List middleware}) {
return addRoute('DELETE', path, handler, middleware: middleware);
}
/// Adds a route that responds to a GET request.
Route get(Pattern path, Object handler, {List middleware}) {
return addRoute('GET', path, handler, middleware: middleware);
}
/// Adds a route that responds to a HEAD request.
Route head(Pattern path, Object handler, {List middleware}) {
return addRoute('HEAD', path, handler, middleware: middleware);
}
/// Adds a route that responds to a OPTIONS request.
Route options(Pattern path, Object handler, {List middleware}) {
return addRoute('OPTIONS', path, handler, middleware: middleware);
}
/// Adds a route that responds to a POST request.
Route post(Pattern path, Object handler, {List middleware}) {
return addRoute('POST', path, handler, middleware: middleware);
}
/// Adds a route that responds to a PATCH request.
Route patch(Pattern path, Object handler, {List middleware}) {
return addRoute('PATCH', path, handler, middleware: middleware);
}
/// Adds a route that responds to a PUT request.
Route put(Pattern path, Object handler, {List middleware}) {
return addRoute('PUT', path, handler, middleware: middleware);
}
}
2016-10-20 09:21:59 +00:00
class _RootRoute extends Route {
2016-11-22 03:31:09 +00:00
_RootRoute() : super("/", method: '*', name: "<root>");
2016-10-20 09:21:59 +00:00
@override
String toString() => "ROOT";
2016-10-21 03:13:13 +00:00
}