fix(router): sort possible routes by cost

This commit is contained in:
Brian Ford 2015-05-12 14:53:13 -07:00
parent 8b6fa1cf19
commit 17392f663f
6 changed files with 78 additions and 33 deletions

View File

@ -20,15 +20,18 @@ export class Instruction {
matchedUrl:string; matchedUrl:string;
params:Map<string, string>; params:Map<string, string>;
reuse:boolean; reuse:boolean;
cost:number;
constructor({params, component, children, matchedUrl}:{params:StringMap, component:any, children:Map, matchedUrl:string} = {}) { constructor({params, component, children, matchedUrl, parentCost}:{params:StringMap, component:any, children:Map, matchedUrl:string, cost:int} = {}) {
this.reuse = false; this.reuse = false;
this.matchedUrl = matchedUrl; this.matchedUrl = matchedUrl;
this.cost = parentCost;
if (isPresent(children)) { if (isPresent(children)) {
this._children = children; this._children = children;
var childUrl; var childUrl;
StringMapWrapper.forEach(this._children, (child, _) => { StringMapWrapper.forEach(this._children, (child, _) => {
childUrl = child.matchedUrl; childUrl = child.matchedUrl;
this.cost += child.cost;
}); });
if (isPresent(childUrl)) { if (isPresent(childUrl)) {
this.matchedUrl += childUrl; this.matchedUrl += childUrl;

View File

@ -52,7 +52,7 @@ class StarSegment {
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$"); var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$"); var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
function parsePathString(route:string):List { function parsePathString(route:string) {
// normalize route as not starting with a "/". Recognition will // normalize route as not starting with a "/". Recognition will
// also normalize. // also normalize.
if (route[0] === "/") { if (route[0] === "/") {
@ -61,6 +61,7 @@ function parsePathString(route:string):List {
var segments = splitBySlash(route); var segments = splitBySlash(route);
var results = ListWrapper.create(); var results = ListWrapper.create();
var cost = 0;
for (var i=0; i<segments.length; i++) { for (var i=0; i<segments.length; i++) {
var segment = segments[i], var segment = segments[i],
@ -68,14 +69,17 @@ function parsePathString(route:string):List {
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) { if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
ListWrapper.push(results, new DynamicSegment(match[1])); ListWrapper.push(results, new DynamicSegment(match[1]));
cost += 100;
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) { } else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
ListWrapper.push(results, new StarSegment(match[1])); ListWrapper.push(results, new StarSegment(match[1]));
cost += 10000;
} else if (segment.length > 0) { } else if (segment.length > 0) {
ListWrapper.push(results, new StaticSegment(segment)); ListWrapper.push(results, new StaticSegment(segment));
cost += 1;
} }
} }
return results; return {segments: results, cost};
} }
var SLASH_RE = RegExpWrapper.create('/'); var SLASH_RE = RegExpWrapper.create('/');
@ -89,12 +93,17 @@ export class PathRecognizer {
segments:List; segments:List;
regex:RegExp; regex:RegExp;
handler:any; handler:any;
cost:number;
constructor(path:string, handler:any) { constructor(path:string, handler:any) {
this.handler = handler; this.handler = handler;
this.segments = ListWrapper.create(); this.segments = ListWrapper.create();
var segments = parsePathString(path); // TODO: use destructuring assignment
// see https://github.com/angular/ts2dart/issues/158
var parsed = parsePathString(path);
var cost = parsed['cost'];
var segments = parsed['segments'];
var regexString = '^'; var regexString = '^';
ListWrapper.forEach(segments, (segment) => { ListWrapper.forEach(segments, (segment) => {
@ -103,6 +112,7 @@ export class PathRecognizer {
this.regex = RegExpWrapper.create(regexString); this.regex = RegExpWrapper.create(regexString);
this.segments = segments; this.segments = segments;
this.cost = cost;
} }
parseParams(url:string):StringMap { parseParams(url:string):StringMap {

View File

@ -39,6 +39,7 @@ export class RouteRecognizer {
var match; var match;
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) { if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
var solution = StringMapWrapper.create(); var solution = StringMapWrapper.create();
StringMapWrapper.set(solution, 'cost', pathRecognizer.cost);
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler); StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url)); StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));

View File

@ -77,14 +77,14 @@ export class RouteRegistry {
return null; return null;
} }
var solutions = componentRecognizer.recognize(url); var componentSolutions = componentRecognizer.recognize(url);
var fullSolutions = ListWrapper.create();
for(var i = 0; i < solutions.length; i++) { for(var i = 0; i < componentSolutions.length; i++) {
var candidate = solutions[i]; var candidate = componentSolutions[i];
if (candidate['unmatchedUrl'].length == 0) { if (candidate['unmatchedUrl'].length == 0) {
return handlerToLeafInstructions(candidate, parentComponent); ListWrapper.push(fullSolutions, handlerToLeafInstructions(candidate, parentComponent));
} } else {
var children = StringMapWrapper.create(), var children = StringMapWrapper.create(),
allMapped = true; allMapped = true;
@ -102,13 +102,20 @@ export class RouteRegistry {
}); });
if (allMapped) { if (allMapped) {
return new Instruction({ ListWrapper.push(fullSolutions, new Instruction({
component: parentComponent, component: parentComponent,
children: children, children: children,
matchedUrl: candidate['matchedUrl'] matchedUrl: candidate['matchedUrl'],
}); parentCost: candidate['cost']
}));
} }
} }
}
if (fullSolutions.length > 0) {
ListWrapper.sort(fullSolutions, (a, b) => a.cost < b.cost ? -1 : 1);
return fullSolutions[0];
}
return null; return null;
} }
@ -127,13 +134,15 @@ function handlerToLeafInstructions(context, parentComponent) {
StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => { StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => {
children[outletName] = new Instruction({ children[outletName] = new Instruction({
component: component, component: component,
params: context['params'] params: context['params'],
parentCost: 0
}); });
}); });
return new Instruction({ return new Instruction({
component: parentComponent, component: parentComponent,
children: children, children: children,
matchedUrl: context['matchedUrl'] matchedUrl: context['matchedUrl'],
parentCost: context['cost']
}); });
} }

View File

@ -23,6 +23,7 @@ export function main() {
recognizer.addConfig('/test', handler); recognizer.addConfig('/test', handler);
expect(recognizer.recognize('/test')[0]).toEqual({ expect(recognizer.recognize('/test')[0]).toEqual({
'cost' : 1,
'handler': { 'components': { 'a': 'b' } }, 'handler': { 'components': { 'a': 'b' } },
'params': {}, 'params': {},
'matchedUrl': '/test', 'matchedUrl': '/test',
@ -34,6 +35,7 @@ export function main() {
recognizer.addConfig('/', handler); recognizer.addConfig('/', handler);
expect(recognizer.recognize('/')[0]).toEqual({ expect(recognizer.recognize('/')[0]).toEqual({
'cost': 0,
'handler': { 'components': { 'a': 'b' } }, 'handler': { 'components': { 'a': 'b' } },
'params': {}, 'params': {},
'matchedUrl': '/', 'matchedUrl': '/',
@ -44,6 +46,7 @@ export function main() {
it('should work with a dynamic segment', () => { it('should work with a dynamic segment', () => {
recognizer.addConfig('/user/:name', handler); recognizer.addConfig('/user/:name', handler);
expect(recognizer.recognize('/user/brian')[0]).toEqual({ expect(recognizer.recognize('/user/brian')[0]).toEqual({
'cost': 101,
'handler': handler, 'handler': handler,
'params': { 'name': 'brian' }, 'params': { 'name': 'brian' },
'matchedUrl': '/user/brian', 'matchedUrl': '/user/brian',
@ -57,6 +60,7 @@ export function main() {
var solutions = recognizer.recognize('/a'); var solutions = recognizer.recognize('/a');
expect(solutions.length).toBe(1); expect(solutions.length).toBe(1);
expect(solutions[0]).toEqual({ expect(solutions[0]).toEqual({
'cost': 1,
'handler': handler, 'handler': handler,
'params': {}, 'params': {},
'matchedUrl': '/b', 'matchedUrl': '/b',

View File

@ -27,6 +27,24 @@ export function main() {
expect(instruction.getChildInstruction('default').component).toBe(DummyCompB); expect(instruction.getChildInstruction('default').component).toBe(DummyCompB);
}); });
it('should prefer static segments to dynamic', () => {
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompB});
registry.config(rootHostComponent, {'path': '/home', 'component': DummyCompA});
var instruction = registry.recognize('/home', rootHostComponent);
expect(instruction.getChildInstruction('default').component).toBe(DummyCompA);
});
it('should prefer dynamic segments to star', () => {
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompA});
registry.config(rootHostComponent, {'path': '/*site', 'component': DummyCompB});
var instruction = registry.recognize('/home', rootHostComponent);
expect(instruction.getChildInstruction('default').component).toBe(DummyCompA);
});
it('should match the full URL recursively', () => { it('should match the full URL recursively', () => {
registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp}); registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});