fix(router): improve route matching priorities

This commit is contained in:
Brian Ford 2015-05-15 02:05:57 -07:00
parent c29ab86d85
commit 5db89071d4
7 changed files with 240 additions and 127 deletions

View File

@ -14,6 +14,9 @@ export class RouteParams {
} }
} }
/**
* An `Instruction` represents the component hierarchy of the application based on a given route
*/
export class Instruction { export class Instruction {
component:any; component:any;
_children:Map<string, Instruction>; _children:Map<string, Instruction>;
@ -24,21 +27,21 @@ export class Instruction {
// the part of the URL captured by this instruction and all children // the part of the URL captured by this instruction and all children
accumulatedUrl:string; accumulatedUrl:string;
params:Map<string, string>; params:StringMap<string, string>;
reuse:boolean; reuse:boolean;
cost:number; specificity:number;
constructor({params, component, children, matchedUrl, parentCost}:{params:StringMap, component:any, children:StringMap, matchedUrl:string, cost:number} = {}) { constructor({params, component, children, matchedUrl, parentSpecificity}:{params:StringMap, component:any, children:Map, matchedUrl:string, parentSpecificity:number} = {}) {
this.reuse = false; this.reuse = false;
this.capturedUrl = matchedUrl; this.capturedUrl = matchedUrl;
this.accumulatedUrl = matchedUrl; this.accumulatedUrl = matchedUrl;
this.cost = parentCost; this.specificity = parentSpecificity;
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.accumulatedUrl; childUrl = child.accumulatedUrl;
this.cost += child.cost; this.specificity += child.specificity;
}); });
if (isPresent(childUrl)) { if (isPresent(childUrl)) {
this.accumulatedUrl += childUrl; this.accumulatedUrl += childUrl;
@ -50,14 +53,20 @@ export class Instruction {
this.params = params; this.params = params;
} }
hasChild(outletName:string):Instruction { hasChild(outletName:string):boolean {
return StringMapWrapper.contains(this._children, outletName); return StringMapWrapper.contains(this._children, outletName);
} }
/**
* Returns the child instruction with the given outlet name
*/
getChild(outletName:string):Instruction { getChild(outletName:string):Instruction {
return StringMapWrapper.get(this._children, outletName); return StringMapWrapper.get(this._children, outletName);
} }
/**
* (child:Instruction, outletName:string) => {}
*/
forEachChild(fn:Function): void { forEachChild(fn:Function): void {
StringMapWrapper.forEach(this._children, fn); StringMapWrapper.forEach(this._children, fn);
} }
@ -65,7 +74,7 @@ export class Instruction {
/** /**
* Does a synchronous, breadth-first traversal of the graph of instructions. * Does a synchronous, breadth-first traversal of the graph of instructions.
* Takes a function with signature: * Takes a function with signature:
* (parent:Instruction, child:Instruction) => {} * (child:Instruction, outletName:string) => {}
*/ */
traverseSync(fn:Function): void { traverseSync(fn:Function): void {
this.forEachChild(fn); this.forEachChild(fn);
@ -74,7 +83,7 @@ export class Instruction {
/** /**
* Takes a currently active instruction and sets a reuse flag on this instruction * Takes a currently active instruction and sets a reuse flag on each of this instruction's children
*/ */
reuseComponentsFrom(oldInstruction:Instruction): void { reuseComponentsFrom(oldInstruction:Instruction): void {
this.traverseSync((childInstruction, outletName) => { this.traverseSync((childInstruction, outletName) => {

View File

@ -62,7 +62,17 @@ function parsePathString(route:string) {
var segments = splitBySlash(route); var segments = splitBySlash(route);
var results = ListWrapper.create(); var results = ListWrapper.create();
var cost = 0; var specificity = 0;
// The "specificity" of a path is used to determine which route is used when multiple routes match a URL.
// Static segments (like "/foo") are the most specific, followed by dynamic segments (like "/:id"). Star segments
// add no specificity. Segments at the start of the path are more specific than proceeding ones.
// The code below uses place values to combine the different types of segments into a single integer that we can
// sort later. Each static segment is worth hundreds of points of specificity (10000, 9900, ..., 200), and each
// dynamic segment is worth single points of specificity (100, 99, ... 2).
if (segments.length > 98) {
throw new BaseException(`'${route}' has more than the maximum supported number of segments.`);
}
for (var i=0; i<segments.length; i++) { for (var i=0; i<segments.length; i++) {
var segment = segments[i], var segment = segments[i],
@ -70,17 +80,16 @@ function parsePathString(route:string) {
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; specificity += (100 - i);
} 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; specificity += 100 * (100 - i);
} }
} }
return {segments: results, cost}; return {segments: results, specificity};
} }
function splitBySlash (url:string):List<string> { function splitBySlash (url:string):List<string> {
@ -93,16 +102,18 @@ export class PathRecognizer {
segments:List; segments:List;
regex:RegExp; regex:RegExp;
handler:any; handler:any;
cost:number; specificity:number;
path:string;
constructor(path:string, handler:any) { constructor(path:string, handler:any) {
this.path = path;
this.handler = handler; this.handler = handler;
this.segments = []; this.segments = [];
// TODO: use destructuring assignment // TODO: use destructuring assignment
// see https://github.com/angular/ts2dart/issues/158 // see https://github.com/angular/ts2dart/issues/158
var parsed = parsePathString(path); var parsed = parsePathString(path);
var cost = parsed['cost']; var specificity = parsed['specificity'];
var segments = parsed['segments']; var segments = parsed['segments'];
var regexString = '^'; var regexString = '^';
@ -112,7 +123,7 @@ export class PathRecognizer {
this.regex = RegExpWrapper.create(regexString); this.regex = RegExpWrapper.create(regexString);
this.segments = segments; this.segments = segments;
this.cost = cost; this.specificity = specificity;
} }
parseParams(url:string):StringMap<string, string> { parseParams(url:string):StringMap<string, string> {

View File

@ -1,8 +1,12 @@
import {RegExp, RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang'; import {RegExp, RegExpWrapper, StringWrapper, isPresent, BaseException} from 'angular2/src/facade/lang';
import {Map, MapWrapper, List, ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; import {Map, MapWrapper, List, ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
import {PathRecognizer} from './path_recognizer'; import {PathRecognizer} from './path_recognizer';
/**
* `RouteRecognizer` is responsible for recognizing routes for a single component.
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of components.
*/
export class RouteRecognizer { export class RouteRecognizer {
names:Map<string, PathRecognizer>; names:Map<string, PathRecognizer>;
redirects:Map<string, string>; redirects:Map<string, string>;
@ -20,14 +24,25 @@ export class RouteRecognizer {
addConfig(path:string, handler:any, alias:string = null): void { addConfig(path:string, handler:any, alias:string = null): void {
var recognizer = new PathRecognizer(path, handler); var recognizer = new PathRecognizer(path, handler);
MapWrapper.forEach(this.matchers, (matcher, _) => {
if (recognizer.regex.toString() == matcher.regex.toString()) {
throw new BaseException(`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
}
});
MapWrapper.set(this.matchers, recognizer.regex, recognizer); MapWrapper.set(this.matchers, recognizer.regex, recognizer);
if (isPresent(alias)) { if (isPresent(alias)) {
MapWrapper.set(this.names, alias, recognizer); MapWrapper.set(this.names, alias, recognizer);
} }
} }
recognize(url:string):List<StringMap> {
var solutions = []; /**
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
*
*/
recognize(url:string):List<RouteMatch> {
var solutions = ListWrapper.create();
MapWrapper.forEach(this.redirects, (target, path) => { MapWrapper.forEach(this.redirects, (target, path) => {
//TODO: "/" redirect case //TODO: "/" redirect case
if (StringWrapper.startsWith(url, path)) { if (StringWrapper.startsWith(url, path)) {
@ -38,21 +53,20 @@ export class RouteRecognizer {
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => { MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
var match; var match;
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) { if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
var solution = StringMapWrapper.create();
StringMapWrapper.set(solution, 'cost', pathRecognizer.cost);
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));
//TODO(btford): determine a good generic way to deal with terminal matches //TODO(btford): determine a good generic way to deal with terminal matches
if (url == '/') { var matchedUrl = '/';
StringMapWrapper.set(solution, 'matchedUrl', '/'); var unmatchedUrl = '';
StringMapWrapper.set(solution, 'unmatchedUrl', ''); if (url != '/') {
} else { matchedUrl = match[0];
StringMapWrapper.set(solution, 'matchedUrl', match[0]); unmatchedUrl = StringWrapper.substring(url, match[0].length);
var unmatchedUrl = StringWrapper.substring(url, match[0].length);
StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl);
} }
ListWrapper.push(solutions, solution); ListWrapper.push(solutions, new RouteMatch({
specificity: pathRecognizer.specificity,
handler: pathRecognizer.handler,
params: pathRecognizer.parseParams(url),
matchedUrl: matchedUrl,
unmatchedUrl: unmatchedUrl
}));
} }
}); });
@ -68,3 +82,20 @@ export class RouteRecognizer {
return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null; return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null;
} }
} }
export class RouteMatch {
specificity:number;
handler:StringMap<string, any>;
params:StringMap<string, string>;
matchedUrl:string;
unmatchedUrl:string;
constructor({specificity, handler, params, matchedUrl, unmatchedUrl}:
{specificity:number, handler:StringMap, params:StringMap, matchedUrl:string, unmatchedUrl:string} = {}) {
this.specificity = specificity;
this.handler = handler;
this.params = params;
this.matchedUrl = matchedUrl;
this.unmatchedUrl = unmatchedUrl;
}
}

View File

@ -1,10 +1,14 @@
import {RouteRecognizer} from './route_recognizer'; import {RouteRecognizer, RouteMatch} from './route_recognizer';
import {Instruction, noopInstruction} from './instruction'; import {Instruction, noopInstruction} from './instruction';
import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
import {isPresent, isBlank, isType, StringWrapper, BaseException} from 'angular2/src/facade/lang'; import {isPresent, isBlank, isType, StringWrapper, BaseException} from 'angular2/src/facade/lang';
import {RouteConfig} from './route_config_impl'; import {RouteConfig} from './route_config_impl';
import {reflector} from 'angular2/src/reflection/reflection'; import {reflector} from 'angular2/src/reflection/reflection';
/**
* The RouteRegistry holds route configurations for each component in an Angular app.
* It is responsible for creating Instructions from URLs, and generating URLs based on route and parameters.
*/
export class RouteRegistry { export class RouteRegistry {
_rules:Map<any, RouteRecognizer>; _rules:Map<any, RouteRecognizer>;
@ -12,6 +16,9 @@ export class RouteRegistry {
this._rules = MapWrapper.create(); this._rules = MapWrapper.create();
} }
/**
* Given a component and a configuration object, add the route to this registry
*/
config(parentComponent, config:StringMap<string, any>): void { config(parentComponent, config:StringMap<string, any>): void {
if (!StringMapWrapper.contains(config, 'path')) { if (!StringMapWrapper.contains(config, 'path')) {
throw new BaseException('Route config does not contain "path"'); throw new BaseException('Route config does not contain "path"');
@ -33,18 +40,19 @@ export class RouteRegistry {
config = normalizeConfig(config); config = normalizeConfig(config);
if (StringMapWrapper.contains(config, 'redirectTo')) { if (StringMapWrapper.contains(config, 'redirectTo')) {
recognizer.addRedirect(StringMapWrapper.get(config, 'path'), StringMapWrapper.get(config, 'redirectTo')); recognizer.addRedirect(config['path'], config['redirectTo']);
return; return;
} }
var components = StringMapWrapper.get(config, 'components'); var components = config['components'];
StringMapWrapper.forEach(components, (component, _) => { StringMapWrapper.forEach(components, (component, _) => this.configFromComponent(component));
this.configFromComponent(component);
});
recognizer.addConfig(config['path'], config, config['as']); recognizer.addConfig(config['path'], config, config['as']);
} }
/**
* Reads the annotations of a component and configures the registry based on them
*/
configFromComponent(component): void { configFromComponent(component): void {
if (!isType(component)) { if (!isType(component)) {
return; return;
@ -61,70 +69,78 @@ export class RouteRegistry {
var annotation = annotations[i]; var annotation = annotations[i];
if (annotation instanceof RouteConfig) { if (annotation instanceof RouteConfig) {
ListWrapper.forEach(annotation.configs, (config) => { ListWrapper.forEach(annotation.configs, (config) => this.config(component, config));
this.config(component, config);
})
} }
} }
} }
} }
/**
* Given a URL and a parent component, return the most specific instruction for navigating
* the application into the state specified by the
*/
recognize(url:string, parentComponent): Instruction { recognize(url:string, parentComponent): Instruction {
var componentRecognizer = MapWrapper.get(this._rules, parentComponent); var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
if (isBlank(componentRecognizer)) { if (isBlank(componentRecognizer)) {
return null; return null;
} }
var componentSolutions = componentRecognizer.recognize(url); // Matches some beginning part of the given URL
var fullSolutions = []; var possibleMatches = componentRecognizer.recognize(url);
for(var i = 0; i < componentSolutions.length; i++) { // A list of instructions that captures all of the given URL
var candidate = componentSolutions[i]; var fullSolutions = ListWrapper.create();
if (candidate['unmatchedUrl'].length == 0) {
ListWrapper.push(fullSolutions, handlerToLeafInstructions(candidate, parentComponent)); for (var i = 0; i < possibleMatches.length; i++) {
var candidate : RouteMatch = possibleMatches[i];
// if the candidate captures all of the URL, add it to our list of solutions
if (candidate.unmatchedUrl.length == 0) {
ListWrapper.push(fullSolutions, routeMatchToInstruction(candidate, parentComponent));
} else { } else {
// otherwise, recursively match the remaining part of the URL against the component's children
var children = StringMapWrapper.create(), var children = StringMapWrapper.create(),
allMapped = true; allChildrenMatch = true,
components = StringMapWrapper.get(candidate.handler, 'components');
var components = candidate['handler']['components'];
var componentNames = StringMapWrapper.keys(components); var componentNames = StringMapWrapper.keys(components);
for (var nameIndex = 0; nameIndex < componentNames.length; nameIndex++) {
var name = componentNames[nameIndex];
var component = StringMapWrapper.get(components, name);
for (var cmpIndex = 0; cmpIndex < componentNames.length; cmpIndex++) { var childInstruction = this.recognize(candidate.unmatchedUrl, component);
var name = componentNames[cmpIndex];
var component = components[name];
var childInstruction = this.recognize(candidate['unmatchedUrl'], component);
if (isPresent(childInstruction)) { if (isPresent(childInstruction)) {
childInstruction.params = candidate['params']; childInstruction.params = candidate.params;
children[name] = childInstruction; children[name] = childInstruction;
} else { } else {
allMapped = false; allChildrenMatch = false;
break; break;
} }
} }
if (allMapped) { if (allChildrenMatch) {
ListWrapper.push(fullSolutions, new Instruction({ ListWrapper.push(fullSolutions, new Instruction({
component: parentComponent, component: parentComponent,
children: children, children: children,
matchedUrl: candidate['matchedUrl'], matchedUrl: candidate.matchedUrl,
parentCost: candidate['cost'] parentSpecificity: candidate.specificity
})); }));
} }
} }
} }
if (fullSolutions.length > 0) { if (fullSolutions.length > 0) {
var lowCost = fullSolutions[0].cost; var mostSpecificSolution = fullSolutions[0];
var lowIndex = 0; for (var solutionIndex = 1; solutionIndex < fullSolutions.length; solutionIndex++) {
for (var solIdx = 1; solIdx < fullSolutions.length; solIdx++) { var solution = fullSolutions[solutionIndex];
if (fullSolutions[solIdx].cost < lowCost) { if (solution.specificity > mostSpecificSolution.specificity) {
lowCost = fullSolutions[solIdx].cost; mostSpecificSolution = solution;
lowIndex = solIdx;
} }
} }
return fullSolutions[lowIndex]; return mostSpecificSolution;
} }
return null; return null;
@ -137,43 +153,49 @@ export class RouteRegistry {
} }
} }
function handlerToLeafInstructions(context, parentComponent): Instruction { function routeMatchToInstruction(routeMatch:RouteMatch, parentComponent): Instruction {
var children = StringMapWrapper.create(); var children = StringMapWrapper.create();
StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => { var components = StringMapWrapper.get(routeMatch.handler, 'components');
StringMapWrapper.forEach(components, (component, outletName) => {
children[outletName] = new Instruction({ children[outletName] = new Instruction({
component: component, component: component,
params: context['params'], params: routeMatch.params,
parentCost: 0 parentSpecificity: 0
}); });
}); });
return new Instruction({ return new Instruction({
component: parentComponent, component: parentComponent,
children: children, children: children,
matchedUrl: context['matchedUrl'], matchedUrl: routeMatch.matchedUrl,
parentCost: context['cost'] parentSpecificity: routeMatch.specificity
}); });
} }
// given:
// { component: Foo }
// mutates the config to:
// { components: { default: Foo } }
function normalizeConfig(config:StringMap<string, any>): StringMap<string, any> {
if (StringMapWrapper.contains(config, 'component')) {
var component = StringMapWrapper.get(config, 'component');
var components = StringMapWrapper.create();
StringMapWrapper.set(components, 'default', component);
var newConfig = StringMapWrapper.create(); /*
StringMapWrapper.set(newConfig, 'components', components); * Given a config object:
* { 'component': Foo }
* Returns a new config object:
* { components: { default: Foo } }
*
* If the config object does not contain a `component` key, the original
* config object is returned.
*/
function normalizeConfig(config:StringMap<string, any>): StringMap<string, any> {
if (!StringMapWrapper.contains(config, 'component')) {
return config;
}
var newConfig = {
'components': {
'default': config['component']
}
};
StringMapWrapper.forEach(config, (value, key) => { StringMapWrapper.forEach(config, (value, key) => {
if (!StringWrapper.equals(key, 'component') && !StringWrapper.equals(key, 'components')) { if (key != 'component' && key != 'components') {
StringMapWrapper.set(newConfig, key, value); newConfig[key] = value;
} }
}); });
return newConfig; return newConfig;
} }
return config;
}

View File

@ -37,7 +37,7 @@ export class Router {
_pipeline:Pipeline; _pipeline:Pipeline;
_registry:RouteRegistry; _registry:RouteRegistry;
_outlets:Map<any, Outlet>; _outlets:Map<any, RouterOutlet>;
_subject:EventEmitter; _subject:EventEmitter;

View File

@ -14,65 +14,78 @@ export function main() {
var handler = { var handler = {
'components': { 'a': 'b' } 'components': { 'a': 'b' }
}; };
var handler2 = {
'components': { 'b': 'c' }
};
beforeEach(() => { beforeEach(() => {
recognizer = new RouteRecognizer(); recognizer = new RouteRecognizer();
}); });
it('should work with a static segment', () => {
it('should recognize a static segment', () => {
recognizer.addConfig('/test', handler); recognizer.addConfig('/test', handler);
expect(recognizer.recognize('/test')[0].handler).toEqual(handler);
expect(recognizer.recognize('/test')[0]).toEqual({
'cost' : 1,
'handler': { 'components': { 'a': 'b' } },
'params': {},
'matchedUrl': '/test',
'unmatchedUrl': ''
});
}); });
it('should work with leading slash', () => {
it('should recognize a single slash', () => {
recognizer.addConfig('/', handler); recognizer.addConfig('/', handler);
var solution = recognizer.recognize('/')[0];
expect(recognizer.recognize('/')[0]).toEqual({ expect(solution.handler).toEqual(handler);
'cost': 0,
'handler': { 'components': { 'a': 'b' } },
'params': {},
'matchedUrl': '/',
'unmatchedUrl': ''
});
}); });
it('should work with a dynamic segment', () => {
it('should recognize a dynamic segment', () => {
recognizer.addConfig('/user/:name', handler); recognizer.addConfig('/user/:name', handler);
expect(recognizer.recognize('/user/brian')[0]).toEqual({ var solution = recognizer.recognize('/user/brian')[0];
'cost': 101, expect(solution.handler).toEqual(handler);
'handler': handler, expect(solution.params).toEqual({ 'name': 'brian' });
'params': { 'name': 'brian' },
'matchedUrl': '/user/brian',
'unmatchedUrl': ''
});
}); });
it('should allow redirects', () => {
it('should recognize a star segment', () => {
recognizer.addConfig('/first/*rest', handler);
var solution = recognizer.recognize('/first/second/third')[0];
expect(solution.handler).toEqual(handler);
expect(solution.params).toEqual({ 'rest': 'second/third' });
});
it('should throw when given two routes that start with the same static segment', () => {
recognizer.addConfig('/hello', handler);
expect(() => recognizer.addConfig('/hello', handler2)).toThrowError(
'Configuration \'/hello\' conflicts with existing route \'/hello\''
);
});
it('should throw when given two routes that have dynamic segments in the same order', () => {
recognizer.addConfig('/hello/:person/how/:doyoudou', handler);
expect(() => recognizer.addConfig('/hello/:friend/how/:areyou', handler2)).toThrowError(
'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\''
);
});
it('should recognize redirects', () => {
recognizer.addRedirect('/a', '/b'); recognizer.addRedirect('/a', '/b');
recognizer.addConfig('/b', handler); recognizer.addConfig('/b', handler);
var solutions = recognizer.recognize('/a'); var solutions = recognizer.recognize('/a');
expect(solutions.length).toBe(1); expect(solutions.length).toBe(1);
expect(solutions[0]).toEqual({
'cost': 1, var solution = solutions[0];
'handler': handler, expect(solution.handler).toEqual(handler);
'params': {}, expect(solution.matchedUrl).toEqual('/b');
'matchedUrl': '/b',
'unmatchedUrl': ''
});
}); });
it('should generate URLs', () => { it('should generate URLs', () => {
recognizer.addConfig('/app/user/:name', handler, 'user'); recognizer.addConfig('/app/user/:name', handler, 'user');
expect(recognizer.generate('user', {'name' : 'misko'})).toEqual('/app/user/misko'); expect(recognizer.generate('user', {'name' : 'misko'})).toEqual('/app/user/misko');
}); });
it('should throw in the absence of required params URLs', () => { it('should throw in the absence of required params URLs', () => {
recognizer.addConfig('/app/user/:name', handler, 'user'); recognizer.addConfig('/app/user/:name', handler, 'user');
expect(() => recognizer.generate('user', {})).toThrowError( expect(() => recognizer.generate('user', {})).toThrowError(

View File

@ -45,6 +45,33 @@ export function main() {
expect(instruction.getChild('default').component).toBe(DummyCompA); expect(instruction.getChild('default').component).toBe(DummyCompA);
}); });
it('should prefer routes with more dynamic segments', () => {
registry.config(rootHostComponent, {'path': '/:first/*rest', 'component': DummyCompA});
registry.config(rootHostComponent, {'path': '/*all', 'component': DummyCompB});
var instruction = registry.recognize('/some/path', rootHostComponent);
expect(instruction.getChild('default').component).toBe(DummyCompA);
});
it('should prefer routes with more static segments', () => {
registry.config(rootHostComponent, {'path': '/first/:second', 'component': DummyCompA});
registry.config(rootHostComponent, {'path': '/:first/:second', 'component': DummyCompB});
var instruction = registry.recognize('/first/second', rootHostComponent);
expect(instruction.getChild('default').component).toBe(DummyCompA);
});
it('should prefer routes with static segments before dynamic segments', () => {
registry.config(rootHostComponent, {'path': '/first/second/:third', 'component': DummyCompB});
registry.config(rootHostComponent, {'path': '/first/:second/third', 'component': DummyCompA});
var instruction = registry.recognize('/first/second/third', rootHostComponent);
expect(instruction.getChild('default').component).toBe(DummyCompB);
});
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});