platform/lib/src/route.dart

283 lines
8.3 KiB
Dart
Raw Normal View History

2016-10-12 17:58:32 +00:00
import 'extensible.dart';
import 'routing_exception.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'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
2016-10-11 18:53:32 +00:00
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
2016-10-12 17:58:32 +00:00
String _matcherify(String path, {bool expand: true}) {
var p = path.replaceAll(new RegExp(r'\/\*$'), "*").replaceAll('/', r'\/');
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;
}
2016-10-11 18:53:32 +00:00
class Route {
final List<Route> _children = [];
final List _handlers = [];
RegExp _matcher;
2016-10-12 17:58:32 +00:00
String _name;
2016-10-11 18:53:32 +00:00
Route _parent;
String _path;
2016-10-12 17:58:32 +00:00
String _pathified;
RegExp _resolver;
2016-10-11 18:53:32 +00:00
List<Route> get children => new List.unmodifiable(_children);
List get handlers => new List.unmodifiable(_handlers);
RegExp get matcher => _matcher;
final String method;
2016-10-12 17:58:32 +00:00
String get name => _name;
2016-10-11 18:53:32 +00:00
Route get parent => _parent;
String get path => _path;
2016-10-12 17:58:32 +00:00
final Extensible state = new Extensible();
Route get absoluteParent {
Route result = this;
while (result.parent != null) result = result.parent;
return result;
}
/// 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
Route(Pattern path,
{Iterable<Route> children: const [],
Iterable handlers: const [],
this.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-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
}
}
factory Route.join(Route parent, Route child) {
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-11 18:53:32 +00:00
return route
2016-10-12 17:58:32 +00:00
.._matcher = new RegExp('$pattern1$separator$pattern2')
.._parent = parent;
}
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
}
Route addChild(Route route, {bool join: true}) {
2016-10-12 17:58:32 +00:00
Route created = join ? new Route.join(this, route) : route.._parent = this;
2016-10-11 18:53:32 +00:00
_children.add(created);
return created;
}
2016-10-12 17:58:32 +00:00
/// Assigns a name to this route.
Route as(String name) => this.._name = name;
2016-10-11 18:53:32 +00:00
Route child(Pattern path,
{Iterable<Route> children: const [],
Iterable handlers: const [],
String method: "GET",
String name: null}) {
final route = new Route(path,
children: children, handlers: handlers, method: method, name: name);
return addChild(route);
}
2016-10-12 17:58:32 +00:00
/// Generates a URI to this route with the given parameters.
String makeUri([Map<String, dynamic> params]) {
String result = _pathified;
if (params != null) {
for (String key in (params.keys)) {
result = result.replaceAll(
new RegExp(":$key" + r"\??"), params[key].toString());
}
}
return result.replaceAll("*", "");
}
Match match(String path) =>
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
/// Extracts route parameters from a given path.
Map parseParameters(String requestPath) {
Map result = {};
2016-10-13 20:50:27 +00:00
Iterable<String> values =
_parseParameters(requestPath.replaceAll(_straySlashes, ''));
Iterable<Match> matches =
_param.allMatches(_pathified.replaceAll(new RegExp('\/'), r'\/'));
2016-10-12 17:58:32 +00:00
for (int i = 0; i < matches.length; i++) {
Match match = matches.elementAt(i);
String paramName = match.group(1);
String value = values.elementAt(i);
num numValue = num.parse(value, (_) => double.NAN);
if (!numValue.isNaN)
result[paramName] = numValue;
else
result[paramName] = value;
}
return result;
}
_parseParameters(String requestPath) sync* {
Match routeMatch = matcher.firstMatch(requestPath);
2016-10-13 20:50:27 +00:00
for (int i = 1; i <= routeMatch.groupCount; i++) yield routeMatch.group(i);
2016-10-12 17:58:32 +00:00
}
Route resolve(String path, [bool filter(Route route)]) {
final _filter = filter ?? (_) => true;
2016-10-11 18:53:32 +00:00
2016-10-13 20:50:27 +00:00
if ((path.isEmpty || path == '.') && _filter(this)) {
2016-10-11 18:53:32 +00:00
return this;
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, '');
if (stub == '/' || stub.isEmpty && _filter(route)) return route;
}
if (_filter(this))
return this;
else
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);
2016-10-13 20:50:27 +00:00
} else if (matcher.hasMatch(path.replaceAll(_straySlashes, '')) ||
_resolver.hasMatch(path.replaceAll(_straySlashes, ''))) {
return this;
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();
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);
else
throw new RoutingException.orphan();
2016-10-13 20:50:27 +00:00
} else if (segments[0] == '.') {
return resolve(segments.skip(1).join('/'), _filter);
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]}';
2016-10-13 20:50:27 +00:00
print('Subpath for $path on ${this.path} = $subPath');
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-11 18:53:32 +00:00
return route;
else {
2016-10-12 17:58:32 +00:00
return route.resolve(segments.skip(1).join('/'), _filter);
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) {
2016-10-13 20:50:27 +00:00
print(
'Full path is $path, path is ${route.path}, matcher: ${route.matcher.pattern}, resolver: ${route._resolver.pattern}');
2016-10-12 17:58:32 +00:00
if ((route.match(path) != null ||
route._resolver.firstMatch(path) != null) &&
_filter(route))
return route;
else if ((route.match('/$path') != null ||
route._resolver.firstMatch('/$path') != null) &&
_filter(route)) return route;
}
2016-10-11 18:53:32 +00:00
return null;
}
}
}