wildcard patch

This commit is contained in:
Tobe O 2017-11-28 16:07:14 -05:00
parent 08c22da8e5
commit 1c5e572688
8 changed files with 94 additions and 65 deletions

View file

@ -1,6 +1,6 @@
part of angel_route.src.router; part of angel_route.src.router;
class _RouteGrammar { class RouteGrammar {
static final Parser<String> notSlash = static final Parser<String> notSlash =
match(new RegExp(r'[^/]+')).value((r) => r.span.text); match(new RegExp(r'[^/]+')).value((r) => r.span.text);
static final Parser<RegExp> regExp = new _RegExpParser(); static final Parser<RegExp> regExp = new _RegExpParser();
@ -8,27 +8,27 @@ class _RouteGrammar {
match(new RegExp(r':([A-Za-z0-9_]+)')) match(new RegExp(r':([A-Za-z0-9_]+)'))
.value((r) => r.span.text.substring(1)); .value((r) => r.span.text.substring(1));
static final Parser<_ParameterSegment> parameterSegment = chain([ static final Parser<ParameterSegment> parameterSegment = chain([
parameterName, parameterName,
match('?').value((r) => true).opt(), match('?').value((r) => true).opt(),
regExp.opt(), regExp.opt(),
]).map((r) { ]).map((r) {
var s = new _ParameterSegment(r.value[0], r.value[2]); var s = new ParameterSegment(r.value[0], r.value[2]);
return r.value[1] == true ? new _OptionalSegment(s) : s; return r.value[1] == true ? new OptionalSegment(s) : s;
}); });
static final Parser<_WildcardSegment> wildcardSegment = static final Parser<WildcardSegment> wildcardSegment =
match('*').value((r) => new _WildcardSegment()); match('*').value((r) => new WildcardSegment());
static final Parser<_ConstantSegment> constantSegment = static final Parser<ConstantSegment> constantSegment =
notSlash.map((r) => new _ConstantSegment(r.value)); notSlash.map((r) => new ConstantSegment(r.value));
static final Parser<_RouteSegment> routeSegment = static final Parser<RouteSegment> routeSegment =
any([parameterSegment, wildcardSegment, constantSegment]); any([parameterSegment, wildcardSegment, constantSegment]);
static final Parser<_RouteDefinition> routeDefinition = routeSegment static final Parser<RouteDefinition> routeDefinition = routeSegment
.separatedBy(match('/')) .separatedBy(match('/'))
.map((r) => new _RouteDefinition(r.value ?? [])) .map((r) => new RouteDefinition(r.value ?? []))
.surroundedBy(match('/').star().opt()); .surroundedBy(match('/').star().opt());
} }
@ -43,34 +43,37 @@ class _RegExpParser extends Parser<RegExp> {
} }
} }
class _RouteDefinition { class RouteDefinition {
final List<_RouteSegment> segments; final List<RouteSegment> segments;
_RouteDefinition(this.segments); RouteDefinition(this.segments);
Parser<Map<String, String>> compile() { Parser<Map<String, dynamic>> compile() {
Parser<Map<String, String>> out; Parser<Map<String, dynamic>> out;
for (var s in segments) { for (int i = 0; i < segments.length; i++) {
var s = segments[i];
bool isLast = i == segments.length - 1;
if (out == null) if (out == null)
out = s.compile(); out = s.compile(isLast);
else else
out = s.compileNext(out.then(match('/')).index(0)); out = s.compileNext(out.then(match('/')).index(0), isLast);
} }
return out; return out;
} }
} }
abstract class _RouteSegment { abstract class RouteSegment {
Parser<Map<String, String>> compile(); Parser<Map<String, dynamic>> compile(bool isLast);
Parser<Map<String, String>> compileNext(Parser<Map<String, String>> p); Parser<Map<String, dynamic>> compileNext(
Parser<Map<String, dynamic>> p, bool isLast);
} }
class _ConstantSegment extends _RouteSegment { class ConstantSegment extends RouteSegment {
final String text; final String text;
_ConstantSegment(this.text); ConstantSegment(this.text);
@override @override
String toString() { String toString() {
@ -78,41 +81,44 @@ class _ConstantSegment extends _RouteSegment {
} }
@override @override
Parser<Map<String, String>> compile() { Parser<Map<String, dynamic>> compile(bool isLast) {
return match(text).value((r) => {}); return match(text).value((r) => {});
} }
@override @override
Parser<Map<String, String>> compileNext(Parser<Map<String, String>> p) { Parser<Map<String, dynamic>> compileNext(
return p.then(compile()).index(0); Parser<Map<String, dynamic>> p, bool isLast) {
return p.then(compile(isLast)).index(0);
} }
} }
class _WildcardSegment extends _RouteSegment { class WildcardSegment extends RouteSegment {
@override @override
String toString() { String toString() {
return 'Wildcard segment'; return 'Wildcard segment';
} }
Parser<Map<String, String>> _compile() { Parser<Map<String, dynamic>> _compile(bool isLast) {
if (isLast) return match(new RegExp(r'.*'));
return match(new RegExp(r'[^/]*')); return match(new RegExp(r'[^/]*'));
} }
@override @override
Parser<Map<String, String>> compile() { Parser<Map<String, dynamic>> compile(bool isLast) {
return _compile().map((r) => {}); return _compile(isLast).map((r) => {});
} }
@override @override
Parser<Map<String, String>> compileNext(Parser<Map<String, String>> p) { Parser<Map<String, dynamic>> compileNext(
return p.then(_compile()).index(0); Parser<Map<String, dynamic>> p, bool isLast) {
return p.then(_compile(isLast)).index(0);
} }
} }
class _OptionalSegment extends _ParameterSegment { class OptionalSegment extends ParameterSegment {
final _ParameterSegment parameter; final ParameterSegment parameter;
_OptionalSegment(this.parameter) : super(parameter.name, parameter.regExp); OptionalSegment(this.parameter) : super(parameter.name, parameter.regExp);
@override @override
String toString() { String toString() {
@ -120,12 +126,13 @@ class _OptionalSegment extends _ParameterSegment {
} }
@override @override
Parser<Map<String, String>> compile() { Parser<Map<String, dynamic>> compile(bool isLast) {
return super.compile().opt(); return super.compile(isLast).opt();
} }
@override @override
Parser<Map<String, String>> compileNext(Parser<Map<String, String>> p) { Parser<Map<String, dynamic>> compileNext(
Parser<Map<String, dynamic>> p, bool isLast) {
return p.then(_compile().opt()).map((r) { return p.then(_compile().opt()).map((r) {
if (r.value[1] == null) return r.value[0]; if (r.value[1] == null) return r.value[0];
return r.value[0]..addAll({name: Uri.decodeComponent(r.value[1])}); return r.value[0]..addAll({name: Uri.decodeComponent(r.value[1])});
@ -133,11 +140,11 @@ class _OptionalSegment extends _ParameterSegment {
} }
} }
class _ParameterSegment extends _RouteSegment { class ParameterSegment extends RouteSegment {
final String name; final String name;
final RegExp regExp; final RegExp regExp;
_ParameterSegment(this.name, this.regExp); ParameterSegment(this.name, this.regExp);
@override @override
String toString() { String toString() {
@ -145,19 +152,20 @@ class _ParameterSegment extends _RouteSegment {
return 'Param: $name'; return 'Param: $name';
} }
Parser<Map<String, String>> _compile() { Parser<Map<String, dynamic>> _compile() {
return regExp != null return regExp != null
? match(regExp).value((r) => r.span.text) ? match(regExp).value((r) => r.span.text)
: _RouteGrammar.notSlash; : RouteGrammar.notSlash;
} }
@override @override
Parser<Map<String, String>> compile() { Parser<Map<String, dynamic>> compile(bool isLast) {
return _compile().map((r) => {name: Uri.decodeComponent(r.span.text)}); return _compile().map((r) => {name: Uri.decodeComponent(r.span.text)});
} }
@override @override
Parser<Map<String, String>> compileNext(Parser<Map<String, String>> p) { Parser<Map<String, dynamic>> compileNext(
Parser<Map<String, dynamic>> p, bool isLast) {
return p.then(_compile()).map((r) { return p.then(_compile()).map((r) {
return r.value[0]..addAll({name: Uri.decodeComponent(r.value[1])}); return r.value[0]..addAll({name: Uri.decodeComponent(r.value[1])});
}); });

View file

@ -5,13 +5,13 @@ class Route {
final String method; final String method;
final String path; final String path;
final List handlers; final List handlers;
final Map<String, Map<String, String>> _cache = {}; final Map<String, Map<String, dynamic>> _cache = {};
final _RouteDefinition _routeDefinition; final RouteDefinition _routeDefinition;
String name; String name;
Parser<Map<String, String>> _parser; Parser<Map<String, dynamic>> _parser;
Route(this.path, {@required this.method, @required this.handlers}) Route(this.path, {@required this.method, @required this.handlers})
: _routeDefinition = _RouteGrammar.routeDefinition : _routeDefinition = RouteGrammar.routeDefinition
.parse(new SpanScanner(path.replaceAll(_straySlashes, ''))) .parse(new SpanScanner(path.replaceAll(_straySlashes, '')))
.value { .value {
if (_routeDefinition.segments.isEmpty) _parser = match('').value((r) => {}); if (_routeDefinition.segments.isEmpty) _parser = match('').value((r) => {});
@ -24,7 +24,7 @@ class Route {
method: b.method, handlers: b.handlers); method: b.method, handlers: b.handlers);
} }
Parser<Map<String, String>> get parser => Parser<Map<String, dynamic>> get parser =>
_parser ??= _routeDefinition.compile(); _parser ??= _routeDefinition.compile();
@ -50,9 +50,9 @@ class Route {
for (var seg in _routeDefinition.segments) { for (var seg in _routeDefinition.segments) {
if (i++ > 0) b.write('/'); if (i++ > 0) b.write('/');
if (seg is _ConstantSegment) if (seg is ConstantSegment)
b.write(seg.text); b.write(seg.text);
else if (seg is _ParameterSegment) { else if (seg is ParameterSegment) {
if (!params.containsKey(seg.name)) if (!params.containsKey(seg.name))
throw new ArgumentError('Missing parameter "${seg.name}".'); throw new ArgumentError('Missing parameter "${seg.name}".');
b.write(params[seg.name]); b.write(params[seg.name]);

View file

@ -27,9 +27,6 @@ class Router {
final List<Route> _routes = []; final List<Route> _routes = [];
bool _useCache = false; bool _useCache = false;
/// Set to `true` to print verbose debug output when interacting with this route.
bool debug = false;
List get middleware => new List.unmodifiable(_middleware); List get middleware => new List.unmodifiable(_middleware);
Map<Pattern, Router> get mounted => Map<Pattern, Router> get mounted =>
@ -57,7 +54,7 @@ class Router {
/// Provide a `root` to make this Router revolve around a pre-defined route. /// Provide a `root` to make this Router revolve around a pre-defined route.
/// Not recommended. /// Not recommended.
Router({this.debug: false}); Router();
/// Enables the use of a cache to eliminate the overhead of consecutive resolutions of the same path. /// Enables the use of a cache to eliminate the overhead of consecutive resolutions of the same path.
void enableCache() { void enableCache() {
@ -97,7 +94,7 @@ class Router {
/// Returns a [Router] with a duplicated version of this tree. /// Returns a [Router] with a duplicated version of this tree.
Router clone() { Router clone() {
final router = new Router(debug: debug); final router = new Router();
final newMounted = new Map<Pattern, Router>.from(mounted); final newMounted = new Map<Pattern, Router>.from(mounted);
for (Route route in routes) { for (Route route in routes) {
@ -169,7 +166,7 @@ class Router {
String name: null, String name: null,
String namespace: null}) { String namespace: null}) {
final router = new Router().._middleware.addAll(middleware); final router = new Router().._middleware.addAll(middleware);
callback(router..debug = debug); callback(router);
return mount(path, router, namespace: namespace)..name = name; return mount(path, router, namespace: namespace)..name = name;
} }
@ -442,7 +439,7 @@ class _ChainedRouter extends Router {
String namespace: null}) { String namespace: null}) {
final router = final router =
new _ChainedRouter(_root, []..addAll(_handlers)..addAll(middleware)); new _ChainedRouter(_root, []..addAll(_handlers)..addAll(middleware));
callback(router..debug = debug); callback(router);
return mount(path, router, namespace: namespace)..name = name; return mount(path, router, namespace: namespace)..name = name;
} }
@ -470,7 +467,7 @@ class _ChainedRouter extends Router {
/// Optimizes a router by condensing all its routes into one level. /// Optimizes a router by condensing all its routes into one level.
Router flatten(Router router) { Router flatten(Router router) {
var flattened = new Router(debug: router.debug == true) var flattened = new Router()
..requestMiddleware.addAll(router.requestMiddleware); ..requestMiddleware.addAll(router.requestMiddleware);
for (var route in router.routes) { for (var route in router.routes) {

View file

@ -3,7 +3,7 @@ part of angel_route.src.router;
/// Represents a complex result of navigating to a path. /// Represents a complex result of navigating to a path.
class RoutingResult { class RoutingResult {
/// The parse result that matched the given sub-path. /// The parse result that matched the given sub-path.
final ParseResult<Map<String, String>> parseResult; final ParseResult<Map<String, dynamic>> parseResult;
/// A nested instance, if a sub-path was matched. /// A nested instance, if a sub-path was matched.
final Iterable<RoutingResult> nested; final Iterable<RoutingResult> nested;

View file

@ -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: 2.0.3+2 version: 2.0.4
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
environment: environment:

View file

@ -2,7 +2,7 @@ import 'package:angel_route/angel_route.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { main() {
final router = new Router(debug: true) final router = new Router()
..get('/hello', '') ..get('/hello', '')
..get('/user/:id', ''); ..get('/user/:id', '');
@ -11,7 +11,7 @@ main() {
router.get('/readers/:readerId', ''); router.get('/readers/:readerId', '');
}); });
router.mount('/color', new Router(debug: true)..get('/:name/shades', '')); router.mount('/color', new Router()..get('/:name/shades', ''));
setUp(router.dumpTree); setUp(router.dumpTree);

View file

@ -11,7 +11,7 @@ const List<Map<String, String>> people = const [
main() { main() {
http.Client client; http.Client client;
final Router router = new Router(debug: true); final Router router = new Router();
HttpServer server; HttpServer server;
String url; String url;

24
test/wildcard_test.dart Normal file
View file

@ -0,0 +1,24 @@
import 'package:angel_route/angel_route.dart';
import 'package:test/test.dart';
void main() {
var router = new Router();
router.get('/songs/*/key', 'of life');
router.get('/isnt/she/*', 'lovely');
router.all('*', 'stevie');
test('match until end if * is last', () {
var result = router.resolveAbsolute('/wonder').first;
expect(result.handlers, ['stevie']);
});
test('match if not last', () {
var result = router.resolveAbsolute('/songs/what/key').first;
expect(result.handlers, ['of life']);
});
test('match if segments before', () {
var result = router.resolveAbsolute('/isnt/she/fierce%20harmonica%solo').first;
expect(result.handlers, ['lovely']);
});
}