platform/lib/src/route.dart

532 lines
16 KiB
Dart
Raw Normal View History

2016-11-22 03:31:09 +00:00
part of angel_route.src.router;
2016-10-11 18:53:32 +00:00
2016-10-12 17:58:32 +00:00
String _matcherify(String path, {bool expand: true}) {
var p = path.replaceAll(new RegExp(r'/\*$'), "*").replaceAll('/', r'\/');
2016-10-12 17:58:32 +00:00
if (expand) {
var match = _param.firstMatch(p);
while (match != null) {
if (match.group(3) == null)
p = p.replaceAll(match[0], '([^\/]+)');
else
p = p.replaceAll(match[0], '(${match[3]})');
match = _param.firstMatch(p);
}
}
p = p.replaceAll(new RegExp('\\*'), '.*');
p = '^$p\$';
return p;
}
String _pathify(String path) {
var p = path.replaceAll(_straySlashes, '');
Map<String, String> replace = {};
for (Match match in _param.allMatches(p)) {
if (match[3] != null) replace[match[0]] = ':${match[1]}';
}
replace.forEach((k, v) {
p = p.replaceAll(k, v);
});
return p;
}
/// Represents a virtual location within an application.
2016-10-11 18:53:32 +00:00
class Route {
final List<Route> _children = [];
final List _handlers = [];
2016-11-22 03:31:09 +00:00
RegExp _head;
2016-10-11 18:53:32 +00:00
RegExp _matcher;
2016-10-14 03:07:34 +00:00
String _method;
2016-10-12 17:58:32 +00:00
String _name;
2016-10-11 18:53:32 +00:00
Route _parent;
RegExp _parentResolver;
2016-10-11 18:53:32 +00:00
String _path;
2016-10-12 17:58:32 +00:00
String _pathified;
RegExp _resolver;
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.
2016-10-11 18:53:32 +00:00
List<Route> get children => new List.unmodifiable(_children);
/// A `List` of arbitrary objects chosen to respond to this request.
2016-10-11 18:53:32 +00:00
List get handlers => new List.unmodifiable(_handlers);
/// A `RegExp` that matches requests to this route.
2016-10-11 18:53:32 +00:00
RegExp get matcher => _matcher;
/// The HTTP method this route is designated for.
2016-10-14 03:07:34 +00:00
String get method => _method;
/// The name of this route, if any.
2016-10-12 17:58:32 +00:00
String get name => _name;
/// The hierarchical parent of this route.
2016-10-11 18:53:32 +00:00
Route get parent => _parent;
/// The virtual path on which this route is mounted.
2016-10-11 18:53:32 +00:00
String get path => _path;
/// Arbitrary state attached to this route.
2016-10-12 17:58:32 +00:00
final Extensible state = new Extensible();
/// The [Route] at the top of the hierarchy this route is found in.
2016-10-12 17:58:32 +00:00
Route get absoluteParent {
Route result = this;
while (result.parent != null) result = result.parent;
return result;
}
2016-11-22 03:31:09 +00:00
/// 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);
}
2016-10-12 17:58:32 +00:00
/// Backtracks up the hierarchy, and builds
/// a sequential list of all handlers from both
/// this route, and every found parent route.
///
/// The resulting list puts handlers from routes
/// higher in the tree at lower indices. Thus,
/// this can be used in a routing-enabled application
/// to evaluate multiple middleware on a single route,
/// and apply them to all children.
List get handlerSequence {
final result = [];
var r = this;
while (r != null) {
result.insertAll(0, r.handlers);
r = r.parent;
}
return result;
}
2016-10-11 18:53:32 +00:00
2016-10-21 03:13:13 +00:00
/// Returns the [Route] instance that will respond to requests
/// to the index of this instance's path.
///
/// May return `this`.
Route get indexRoute {
return children.firstWhere((r) => r.path.replaceAll(path, '').isEmpty,
orElse: () => this);
}
2016-11-22 03:31:09 +00:00
Route._base();
2016-10-11 18:53:32 +00:00
Route(Pattern path,
{Iterable<Route> children: const [],
this.debug: false,
2016-10-11 18:53:32 +00:00
Iterable handlers: const [],
2016-10-14 03:07:34 +00:00
method: "GET",
2016-10-12 17:58:32 +00:00
String name: null}) {
2016-10-11 18:53:32 +00:00
if (children != null) _children.addAll(children);
if (handlers != null) _handlers.addAll(handlers);
2016-10-14 03:07:34 +00:00
_method = method;
2016-10-12 17:58:32 +00:00
_name = name;
2016-10-11 18:53:32 +00:00
if (path is RegExp) {
_matcher = path;
_path = path.pattern;
} else {
2016-10-12 17:58:32 +00:00
_matcher = new RegExp(
_matcherify(path.toString().replaceAll(_straySlashes, '')));
_path = _pathified = _pathify(path.toString());
_resolver = new RegExp(_matcherify(
path.toString().replaceAll(_straySlashes, ''),
expand: false));
2016-10-11 18:53:32 +00:00
}
_parentResolver = new RegExp(_matcher.pattern.replaceAll(_rgxEnd, ''));
2016-10-11 18:53:32 +00:00
}
2016-10-14 03:07:34 +00:00
/// Splits a route path into a list of segments, and then
/// builds a hierarchy of off that.
///
/// This should generally be used instead of the original
/// Route constructor.
///
/// All children and handlers, as well as the method, will be
/// assigned to the last child route created.
///
/// The final child route is returned.
factory Route.build(Pattern path,
{Iterable<Route> children: const [],
2016-10-21 03:13:13 +00:00
bool debug: false,
2016-10-14 03:07:34 +00:00
Iterable handlers: const [],
method: "GET",
String name: null}) {
Route result;
2016-10-23 00:52:28 +00:00
if (path is RegExp) {
2016-11-23 18:58:34 +00:00
result = new Route(path,
debug: debug, handlers: handlers, method: method, name: name);
2016-10-23 00:52:28 +00:00
} else {
final segments = path
.toString()
.split('/')
.where((str) => str.isNotEmpty)
.toList(growable: false);
2016-10-23 00:52:28 +00:00
if (segments.isEmpty) {
return new Route('/',
children: children,
debug: debug,
handlers: handlers,
method: method,
name: name);
}
2016-11-22 03:31:09 +00:00
var head = '';
2016-10-23 00:52:28 +00:00
for (int i = 0; i < segments.length; i++) {
final segment = segments[i];
2016-11-22 03:31:09 +00:00
head = (head + '/$segment').replaceAll(_straySlashes, '');
2016-10-23 00:52:28 +00:00
if (i == segments.length - 1) {
if (result == null) {
result = new Route(segment, debug: debug);
} else {
result = result.child(segment, debug: debug);
}
} else {
2016-10-23 00:52:28 +00:00
if (result == null) {
result = new Route(segment, debug: debug, method: "*");
} else {
result = result.child(segment, debug: debug, method: "*");
}
}
2016-11-22 03:31:09 +00:00
result._head = new RegExp(_matcherify(head).replaceAll(_rgxEnd, ''));
}
2016-10-14 03:07:34 +00:00
}
result._children.addAll(children);
result._handlers.addAll(handlers);
result._method = method;
result._name = name;
2016-10-21 03:13:13 +00:00
return result..debug = debug;
2016-10-14 03:07:34 +00:00
}
/// Combines the paths and matchers of two [Route] instances, and creates a new instance.
2016-10-21 03:13:13 +00:00
factory Route.join(Route parent, Route child, {bool debug: false}) {
2016-10-12 17:58:32 +00:00
final String path1 = parent.path
.replaceAll(_rgxStart, '')
.replaceAll(_rgxEnd, '')
.replaceAll(_straySlashes, '');
final String path2 = child.path
.replaceAll(_rgxStart, '')
.replaceAll(_rgxEnd, '')
.replaceAll(_straySlashes, '');
final String pattern1 = parent.matcher.pattern
.replaceAll(_rgxEnd, '')
.replaceAll(_rgxStraySlashes, '');
final String pattern2 = child.matcher.pattern
.replaceAll(_rgxStart, '')
.replaceAll(_rgxStraySlashes, '');
2016-10-11 18:53:32 +00:00
2016-10-12 17:58:32 +00:00
final route = new Route('$path1/$path2',
2016-10-11 18:53:32 +00:00
children: child.children,
handlers: child.handlers,
method: child.method,
name: child.name);
2016-10-12 17:58:32 +00:00
String separator = (pattern1.isEmpty || pattern1 == '^') ? '' : '\\/';
2016-10-14 03:07:34 +00:00
parent._children.add(route
2016-10-12 17:58:32 +00:00
.._matcher = new RegExp('$pattern1$separator$pattern2')
2016-11-23 18:58:34 +00:00
.._head = new RegExp(
_matcherify('$path1/$path2'.replaceAll(_straySlashes, ''))
.replaceAll(_rgxEnd, ''))
.._parent = parent
.._stub = child.matcher);
2016-10-21 03:13:13 +00:00
2016-11-23 20:55:51 +00:00
/*parent._printDebug(
"Joined '/$path1' and '/$path2', created head: ${route._head.pattern} and stub: ${route._stub.pattern}");*/
2016-10-14 03:07:34 +00:00
2016-10-21 03:13:13 +00:00
return route..debug = parent.debug || child.debug || debug;
}
/// Calls [addChild] on all given routes.
2016-10-12 17:58:32 +00:00
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
return routes.map((route) => addChild(route, join: join)).toList();
2016-10-11 18:53:32 +00:00
}
/// Adds the given route as a hierarchical child of this one.
2016-10-11 18:53:32 +00:00
Route addChild(Route route, {bool join: true}) {
2016-10-21 03:13:13 +00:00
Route created;
if (join) {
created = new Route.join(this, route);
} else {
_children.add(created = route.._parent = this);
}
2016-10-20 09:21:59 +00:00
return created..debug = debug;
2016-10-11 18:53:32 +00:00
}
2016-10-12 17:58:32 +00:00
/// Assigns a name to this route.
Route as(String name) => this.._name = name;
/// Creates a hierarchical child of this route with the given path.
2016-10-11 18:53:32 +00:00
Route child(Pattern path,
{Iterable<Route> children: const [],
2016-10-21 03:13:13 +00:00
bool debug: false,
2016-10-11 18:53:32 +00:00
Iterable handlers: const [],
String method: "GET",
String name: null}) {
2016-10-14 03:07:34 +00:00
final route = new Route.build(path,
2016-10-21 03:13:13 +00:00
children: children,
debug: debug,
handlers: handlers,
method: method,
name: name);
2016-10-11 18:53:32 +00:00
return addChild(route);
}
2016-11-23 18:58:34 +00:00
Route clone() {
2016-11-27 23:39:03 +00:00
final Route route = new Route('', debug: debug);
2016-11-23 18:58:34 +00:00
return route
.._children.addAll(children)
.._handlers.addAll(handlers)
.._head = _head
.._matcher = _matcher
.._method = _method
.._name = name
.._parent = _parent
.._parentResolver = _parentResolver
2016-11-27 23:39:03 +00:00
.._path = _path
2016-11-23 18:58:34 +00:00
.._pathified = _pathified
.._resolver = _resolver
.._stub = _stub
..state.properties.addAll(state.properties);
}
2016-10-12 17:58:32 +00:00
/// Generates a URI to this route with the given parameters.
String makeUri([Map<String, dynamic> params]) {
2016-11-23 09:13:42 +00:00
String result = _pathify(path);
2016-10-12 17:58:32 +00:00
if (params != null) {
for (String key in (params.keys)) {
result = result.replaceAll(
new RegExp(":$key" + r"\??"), params[key].toString());
}
}
return result.replaceAll("*", "");
}
/// Attempts to match a path against this route.
2016-10-12 17:58:32 +00:00
Match match(String path) =>
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
/// Extracts route parameters from a given path.
2016-12-21 23:28:00 +00:00
Map<String, String> parseParameters(String requestPath) {
Map<String, String> result = {};
2016-10-12 17:58:32 +00:00
2016-10-13 20:50:27 +00:00
Iterable<String> values =
2016-11-25 23:22:33 +00:00
_parseParameters(requestPath.replaceAll(_straySlashes, ''));
2016-11-23 09:13:42 +00:00
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Searched request path $requestPath and found these values: $values');
// _printDebug('This route\'s path is "$path".');
2016-11-23 09:13:42 +00:00
final pathString = _pathify(path).replaceAll(new RegExp('\/'), r'\/');
2016-11-23 18:58:34 +00:00
Iterable<Match> matches = _param.allMatches(pathString);
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'All param names parsed in "$pathString": ${matches.map((m) => m[1])}');
2016-11-23 09:13:42 +00:00
for (int i = 0; i < matches.length && i < values.length; i++) {
2016-10-12 17:58:32 +00:00
Match match = matches.elementAt(i);
String paramName = match.group(1);
String value = values.elementAt(i);
2017-06-06 12:48:55 +00:00
// _printDebug('Setting param "$paramName" to "$value"...');
2016-11-27 23:39:03 +00:00
result[paramName] = value;
2016-10-12 17:58:32 +00:00
}
return result;
}
_parseParameters(String requestPath) sync* {
Match routeMatch = matcher.firstMatch(requestPath);
if (routeMatch != null)
for (int i = 1; i <= routeMatch.groupCount; i++)
yield routeMatch.group(i);
2016-10-12 17:58:32 +00:00
}
/// 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}) {
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Path to resolve: "/${path.replaceAll(_straySlashes, '')}", our matcher: ${matcher.pattern}');
2016-10-21 03:13:13 +00:00
bool _filter(route) {
if (filter == null) {
2017-06-06 12:48:55 +00:00
// _printDebug('No filter provided, returning true for $route');
2016-10-21 03:13:13 +00:00
return true;
} else {
2017-06-06 12:48:55 +00:00
// _printDebug('Running filter on $route');
2016-10-21 03:13:13 +00:00
final result = filter(route);
2017-06-06 12:48:55 +00:00
// _printDebug('Filter result: $result');
2016-10-21 03:13:13 +00:00
return result;
}
}
final _fullPath = fullPath ?? path;
2016-10-11 18:53:32 +00:00
2016-10-21 03:13:13 +00:00
if ((path.isEmpty || path == '.') && _filter(indexRoute)) {
// Try to find index
2017-06-06 12:48:55 +00:00
// _printDebug('Empty path, resolving with indexRoute: $indexRoute');
2016-10-21 03:13:13 +00:00
return indexRoute;
} else if (path == '/') {
return absoluteParent.resolve('');
2016-10-12 17:58:32 +00:00
} else if (path.replaceAll(_straySlashes, '').isEmpty) {
for (Route route in children) {
final stub = route.path.replaceAll(this.path, '');
2016-10-21 03:13:13 +00:00
if ((stub == '/' || stub.isEmpty) && _filter(route))
return route.resolve('');
2016-10-12 17:58:32 +00:00
}
2016-10-21 03:13:13 +00:00
if (_filter(indexRoute)) {
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Path "/$path" is technically empty, sending to indexRoute: $indexRoute');
2016-10-21 03:13:13 +00:00
return indexRoute;
} else
2016-10-12 17:58:32 +00:00
return null;
} else if (path == '..') {
if (parent != null)
return parent;
else
throw new RoutingException.orphan();
} else if (path.startsWith('/') &&
path.length > 1 &&
path[1] != '/' &&
absoluteParent != null) {
return absoluteParent.resolve(path.substring(1),
filter: _filter, fullPath: _fullPath);
2016-10-13 20:50:27 +00:00
} else if (matcher.hasMatch(path.replaceAll(_straySlashes, '')) ||
_resolver.hasMatch(path.replaceAll(_straySlashes, ''))) {
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Path "/$path" matched our matcher, sending to indexRoute: $indexRoute');
2016-10-21 03:13:13 +00:00
return indexRoute;
2016-10-11 18:53:32 +00:00
} else {
2016-10-13 20:50:27 +00:00
final segments = path.split('/').where((str) => str.isNotEmpty).toList();
2017-06-06 12:48:55 +00:00
// _printDebug('Segments: $segments on "/${this.path}"');
if (segments.isEmpty) {
2017-06-06 12:48:55 +00:00
// _printDebug('Empty segments, sending to indexRoute: $indexRoute');
2016-10-21 03:13:13 +00:00
return indexRoute;
}
2016-10-11 18:53:32 +00:00
2016-10-12 17:58:32 +00:00
if (segments[0] == '..') {
if (parent != null)
return parent.resolve(segments.skip(1).join('/'),
filter: _filter, fullPath: _fullPath);
2016-10-12 17:58:32 +00:00
else
throw new RoutingException.orphan();
2016-10-13 20:50:27 +00:00
} else if (segments[0] == '.') {
return resolve(segments.skip(1).join('/'),
filter: _filter, fullPath: _fullPath);
2016-10-12 17:58:32 +00:00
}
2016-10-11 18:53:32 +00:00
for (Route route in children) {
final subPath = '${this.path}/${segments[0]}';
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'seg0: ${segments[0]}, stub: ${route._stub?.pattern}, path: $path, route.path: ${route.path}, route.matcher: ${route.matcher.pattern}, this.matcher: ${matcher.pattern}');
2016-10-11 18:53:32 +00:00
2016-10-12 17:58:32 +00:00
if (route.match(subPath) != null ||
route._resolver.firstMatch(subPath) != null) {
if (segments.length == 1 && _filter(route))
2016-10-21 03:13:13 +00:00
return route.resolve('');
2016-10-11 18:53:32 +00:00
else {
return route.resolve(segments.skip(1).join('/'),
filter: _filter,
fullPath: this.path.replaceAll(_straySlashes, '') +
'/' +
_fullPath.replaceAll(_straySlashes, ''));
}
} else if (route._stub != null && route._stub.hasMatch(segments[0])) {
2016-10-21 03:13:13 +00:00
if (segments.length == 1) {
2017-06-06 12:48:55 +00:00
// _printDebug('Stub perhaps matches');
2016-10-21 03:13:13 +00:00
return route.resolve('');
} else {
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Maybe stub matches. Sending remaining segments to $route');
2016-10-21 03:13:13 +00:00
return route.resolve(segments.skip(1).join('/'));
}
}
}
// Try to match "subdirectory"
for (Route route in children) {
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Trying to match subdir for $path; child ${route.path} on ${this.path}');
final match = route._parentResolver.firstMatch(path);
if (match != null) {
final subPath =
2016-11-25 23:22:33 +00:00
path.replaceFirst(match[0], '').replaceAll(_straySlashes, '');
2017-06-06 12:48:55 +00:00
// _printDebug("Subdir path: $subPath");
for (Route child in route.children) {
final testPath = child.path
.replaceFirst(route.path, '')
.replaceAll(_straySlashes, '');
if (subPath == testPath &&
(child.match(_fullPath) != null ||
child._resolver.firstMatch(_fullPath) != null) &&
_filter(child)) {
2016-10-21 03:13:13 +00:00
return child.resolve('');
}
2016-10-11 18:53:32 +00:00
}
}
2016-10-21 03:13:13 +00:00
}
2016-10-11 18:53:32 +00:00
2016-10-12 17:58:32 +00:00
// Try to match the whole route, if nothing else works
for (Route route in children) {
2017-06-06 12:48:55 +00:00
// _printDebug(
// 'Trying to match full $_fullPath for ${route.path} on ${this.path}');
if ((route.match(_fullPath) != null ||
2016-11-25 23:22:33 +00:00
route._resolver.firstMatch(_fullPath) != null) &&
2016-10-23 00:52:28 +00:00
_filter(route)) {
2017-06-06 12:48:55 +00:00
// _printDebug('Matched full path!');
2016-10-21 03:13:13 +00:00
return route.resolve('');
2016-10-23 00:52:28 +00:00
} else if ((route.match('/$_fullPath') != null ||
2016-11-25 23:22:33 +00:00
route._resolver.firstMatch('/$_fullPath') != null) &&
2016-10-23 00:52:28 +00:00
_filter(route)) {
2017-06-06 12:48:55 +00:00
// _printDebug('Matched full path (with a leading slash!)');
2016-10-23 00:52:28 +00:00
return route.resolve('');
}
2016-10-21 03:13:13 +00:00
}
// Lastly, check to see if we have an index route to resolve with
if (indexRoute != this) {
2017-06-06 12:48:55 +00:00
// _printDebug('Forwarding "/$path" to indexRoute');
2016-10-21 03:13:13 +00:00
return indexRoute.resolve(path);
2016-10-12 17:58:32 +00:00
}
2016-10-11 18:53:32 +00:00
return null;
}
}
@override
String toString() => "$method '$path' => ${handlers.length} handler(s)";
2016-11-25 23:22:33 +00:00
}