2016-11-22 03:31:09 +00:00
|
|
|
library angel_route.src.router;
|
|
|
|
|
2016-10-12 17:58:32 +00:00
|
|
|
import 'extensible.dart';
|
2016-10-19 22:04:06 +00:00
|
|
|
import 'routing_exception.dart';
|
2016-11-25 23:22:33 +00:00
|
|
|
part 'symlink_route.dart';
|
2016-11-22 03:31:09 +00:00
|
|
|
part 'route.dart';
|
2016-11-25 23:22:33 +00:00
|
|
|
part 'routing_result.dart';
|
2016-11-22 03:31:09 +00:00
|
|
|
|
|
|
|
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
|
|
|
final RegExp _rgxEnd = new RegExp(r'\$+$');
|
|
|
|
final RegExp _rgxStart = new RegExp(r'^\^+');
|
2016-11-23 09:13:42 +00:00
|
|
|
final RegExp _rgxStraySlashes =
|
|
|
|
new RegExp(r'(^((\\+/)|(/))+)|(((\\+/)|(/))+$)');
|
2016-11-22 03:31:09 +00:00
|
|
|
final RegExp _slashDollar = new RegExp(r'/+\$');
|
2016-10-12 17:58:32 +00:00
|
|
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
|
|
|
|
2016-10-19 22:04:06 +00:00
|
|
|
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
|
2016-11-25 23:22:33 +00:00
|
|
|
class Router {
|
|
|
|
final List _middleware = [];
|
|
|
|
final Map<Pattern, Router> _mounted = {};
|
|
|
|
final List<Route> _routes = [];
|
2016-10-21 03:13:13 +00:00
|
|
|
|
2016-10-19 22:04:06 +00:00
|
|
|
/// Set to `true` to print verbose debug output when interacting with this route.
|
|
|
|
bool debug = false;
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
List get middleware => new List.unmodifiable(_middleware);
|
|
|
|
|
|
|
|
Map<Pattern, Router> get mounted =>
|
|
|
|
new Map<Pattern, Router>.unmodifiable(_mounted);
|
|
|
|
|
2016-10-12 17:58:32 +00:00
|
|
|
/// Additional filters to be run on designated requests.
|
|
|
|
Map<String, dynamic> requestMiddleware = {};
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
List<Route> get routes => new List<Route>.unmodifiable(_routes);
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-10-19 22:04:06 +00:00
|
|
|
/// Provide a `root` to make this Router revolve around a pre-defined route.
|
|
|
|
/// Not recommended.
|
2016-11-25 23:22:33 +00:00
|
|
|
Router({this.debug: false});
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-10-19 22:04:06 +00:00
|
|
|
void _printDebug(msg) {
|
2016-11-23 19:06:54 +00:00
|
|
|
if (debug == true) print(msg);
|
2016-10-19 22:04:06 +00:00
|
|
|
}
|
|
|
|
|
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,
|
2016-11-25 23:22:33 +00:00
|
|
|
{List middleware: const []}) {
|
|
|
|
// Check if any mounted routers can match this
|
|
|
|
final handlers = [handler];
|
2016-10-19 22:04:06 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
if (middleware != null) handlers.addAll(middleware);
|
2016-10-19 22:04:06 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
final route =
|
|
|
|
new Route(path, debug: debug, method: method, handlers: handlers);
|
|
|
|
_routes.add(route);
|
|
|
|
return route;
|
2016-10-12 17:58:32 +00:00
|
|
|
}
|
|
|
|
|
2016-11-23 18:58:34 +00:00
|
|
|
/// Returns a [Router] with a duplicated version of this tree.
|
2016-11-25 23:22:33 +00:00
|
|
|
Router clone() {
|
2016-11-23 18:58:34 +00:00
|
|
|
final router = new Router(debug: debug);
|
2016-11-25 23:22:33 +00:00
|
|
|
final newMounted = new Map.from(mounted);
|
|
|
|
|
|
|
|
for (Route route in routes) {
|
|
|
|
if (route is! SymlinkRoute) {
|
|
|
|
router._routes.add(route.clone());
|
|
|
|
} else if (route is SymlinkRoute) {
|
|
|
|
router._routes.add(new SymlinkRoute(route.path, route.pattern,
|
|
|
|
newMounted[route.pattern] = route.router.clone()));
|
|
|
|
}
|
2016-11-23 18:58:34 +00:00
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
return router.._mounted.addAll(newMounted);
|
2016-11-23 18:58:34 +00:00
|
|
|
}
|
|
|
|
|
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(
|
2016-11-23 09:13:42 +00:00
|
|
|
{callback(String tree),
|
2016-11-25 23:22:33 +00:00
|
|
|
String header: 'Dumping route tree:',
|
|
|
|
String tab: ' ',
|
|
|
|
bool showMatchers: false}) {
|
2016-10-12 17:58:32 +00:00
|
|
|
final buf = new StringBuffer();
|
2016-11-25 23:22:33 +00:00
|
|
|
int tabs = 0;
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
if (header != null && header.isNotEmpty) {
|
|
|
|
buf.writeln(header);
|
|
|
|
}
|
2016-10-20 09:21:59 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
indent() {
|
|
|
|
for (int i = 0; i < tabs; i++) buf.write(tab);
|
|
|
|
}
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
dumpRouter(Router router) {
|
|
|
|
indent();
|
|
|
|
buf.writeln('- <root>');
|
|
|
|
tabs++;
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
for (Route route in router.routes) {
|
|
|
|
indent();
|
|
|
|
buf.write('- ${route.path.isNotEmpty ? route.path : '/'}');
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
if (route is SymlinkRoute) {
|
|
|
|
buf.writeln();
|
|
|
|
tabs++;
|
|
|
|
dumpRouter(route.router);
|
|
|
|
tabs--;
|
|
|
|
} else {
|
|
|
|
if (showMatchers) buf.write(' (${route.matcher.pattern})');
|
2016-11-23 09:13:42 +00:00
|
|
|
|
2016-10-23 00:52:28 +00:00
|
|
|
buf.writeln(' => ${route.handlers.length} handler(s)');
|
2016-11-25 23:22:33 +00:00
|
|
|
}
|
2016-10-23 00:52:28 +00:00
|
|
|
}
|
2016-10-12 17:58:32 +00:00
|
|
|
|
|
|
|
tabs--;
|
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
dumpRouter(this);
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-10-19 22:04:06 +00:00
|
|
|
(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.
|
2016-11-25 23:22:33 +00:00
|
|
|
SymlinkRoute group(Pattern path, void callback(Router router),
|
2016-10-12 17:58:32 +00:00
|
|
|
{Iterable middleware: const [],
|
|
|
|
String name: null,
|
|
|
|
String namespace: null}) {
|
2016-11-25 23:22:33 +00:00
|
|
|
final router = new Router(debug: debug).._middleware.addAll(middleware);
|
2016-10-12 17:58:32 +00:00
|
|
|
callback(router);
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
return mount(path, router, namespace: namespace).._name = name;
|
|
|
|
}
|
2016-10-12 17:58:32 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
/// 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>.');
|
2016-10-12 17:58:32 +00:00
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
return absolute
|
|
|
|
? '/${segments.join('/').replaceAll(_straySlashes, '')}'
|
|
|
|
: segments.join('/');
|
2016-10-12 17:58:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Assigns a middleware to a name for convenience.
|
|
|
|
registerMiddleware(String name, middleware) {
|
|
|
|
requestMiddleware[name] = middleware;
|
|
|
|
}
|
|
|
|
|
2016-11-22 03:31:09 +00:00
|
|
|
/// Finds the first [Route] that matches the given path,
|
|
|
|
/// with the given method.
|
2016-11-25 23:22:33 +00:00
|
|
|
RoutingResult resolve(String fullPath, String path, {String method: 'GET'}) {
|
|
|
|
final cleanFullPath = fullPath.replaceAll(_straySlashes, '');
|
|
|
|
final cleanPath = path.replaceAll(_straySlashes, '');
|
2016-11-22 03:31:09 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
for (Route route in routes) {
|
|
|
|
if (route is SymlinkRoute && route._head != null) {
|
|
|
|
final match = route._head.firstMatch(cleanFullPath);
|
2016-11-22 03:31:09 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
if (match != null) {
|
|
|
|
final tail = cleanPath
|
|
|
|
.replaceFirst(match[0], '')
|
2016-11-22 03:31:09 +00:00
|
|
|
.replaceAll(_straySlashes, '');
|
2016-11-25 23:22:33 +00:00
|
|
|
_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);
|
2016-11-23 09:13:42 +00:00
|
|
|
}
|
2016-11-25 23:22:33 +00:00
|
|
|
} 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);
|
2016-11-22 03:31:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
return null;
|
2016-11-23 18:58:34 +00:00
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
/// Finds every possible [Route] that matches the given path,
|
|
|
|
/// with the given method.
|
|
|
|
Iterable<RoutingResult> resolveAll(String fullPath, String path,
|
|
|
|
{String method: 'GET'}) {
|
|
|
|
final router = clone();
|
|
|
|
final List<RoutingResult> results = [];
|
|
|
|
var result = router.resolve(fullPath, path, method: method);
|
|
|
|
|
|
|
|
while (result != null) {
|
|
|
|
results.add(result);
|
|
|
|
result.deepestRouter._routes.remove(result.deepestRoute);
|
|
|
|
result = router.resolve(fullPath, path, method: method);
|
2016-11-22 03:31:09 +00:00
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
_validHead(RegExp rgx) {
|
|
|
|
return !rgx.hasMatch('');
|
2016-11-22 03:31:09 +00:00
|
|
|
}
|
|
|
|
|
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-11-25 23:22:33 +00:00
|
|
|
SymlinkRoute 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-23 09:13:42 +00:00
|
|
|
copiedMiddleware[middlewareName];
|
2016-11-22 03:31:09 +00:00
|
|
|
}
|
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
final route = new SymlinkRoute(path, path, _mounted[path] = router);
|
|
|
|
_routes.add(route);
|
|
|
|
route._head = new RegExp(route.matcher.pattern.replaceAll(_rgxEnd, ''));
|
2016-11-22 03:31:09 +00:00
|
|
|
|
2016-11-25 23:22:33 +00:00
|
|
|
return route.._name = namespace;
|
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);
|
|
|
|
}
|
|
|
|
}
|