Skip to content

Commit 73a1fe0

Browse files
fix(urlService): Fix priority sorting of URL rules
In some cases, URL rules were not sorted in the correct order. This caused, for instance, paths with parameters to be matched over paths with static strings. Closes #66
1 parent 83d70fc commit 73a1fe0

File tree

2 files changed

+51
-13
lines changed

2 files changed

+51
-13
lines changed

src/url/urlMatcher.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,18 @@ export class UrlMatcher {
525525
if (segment instanceof Param) return 3;
526526
});
527527

528-
let cmp, i, pairs = arrayTuples(weights(a), weights(b));
528+
/**
529+
* Pads shorter array in-place (mutates)
530+
*/
531+
const padArrays = (l: any[], r: any[], padVal: any) => {
532+
const len = Math.max(l.length, r.length);
533+
while (l.length < len) l.push(padVal);
534+
while (r.length < len) r.push(padVal);
535+
};
536+
537+
const weightsA = weights(a), weightsB = weights(b);
538+
padArrays(weightsA, weightsB, 0);
539+
let cmp, i, pairs = arrayTuples(weightsA, weightsB);
529540

530541
for (i = 0; i < pairs.length; i++) {
531542
cmp = pairs[i][0] - pairs[i][1];

src/url/urlRouter.ts

+39-12
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
* @module url
44
*/
55
/** for typedoc */
6-
import { composeSort, createProxyFunctions, extend, inArray, removeFrom, sortBy } from '../common/common';
6+
import { createProxyFunctions, extend, removeFrom } from '../common/common';
77
import { isDefined, isFunction, isString } from '../common/predicates';
88
import { UrlMatcher } from './urlMatcher';
99
import { RawParams } from '../params/interface';
1010
import { Disposable } from '../interface';
1111
import { UIRouter } from '../router';
12-
import { is, pattern, pipe, prop, val } from '../common/hof';
12+
import { is, pattern, val } from '../common/hof';
1313
import { UrlRuleFactory } from './urlRule';
1414
import { TargetState } from '../state/targetState';
15-
import { MatchResult, UrlParts, UrlRule, UrlRuleHandlerFn, UrlRuleMatchFn, UrlRulesApi, UrlSyncApi } from './interface';
15+
import { MatcherUrlRule, MatchResult, UrlParts, UrlRule, UrlRuleHandlerFn, UrlRuleMatchFn, UrlRulesApi, UrlSyncApi, } from './interface';
1616
import { TargetStateDef } from '../state/interface';
1717

1818
/** @hidden */
@@ -24,7 +24,26 @@ function appendBasePath(url: string, isHtml5: boolean, absolute: boolean, baseHr
2424
}
2525

2626
/** @hidden */
27-
const getMatcher = prop("urlMatcher");
27+
const prioritySort = (a: UrlRule, b: UrlRule) =>
28+
(b.priority || 0) - (a.priority || 0);
29+
30+
/** @hidden */
31+
const typeSort = (a: UrlRule, b: UrlRule) => {
32+
const weights = { "STATE": 4, "URLMATCHER": 4, "REGEXP": 3, "RAW": 2, "OTHER": 1 };
33+
return (weights[a.type] || 0) - (weights[b.type] || 0);
34+
};
35+
36+
/** @hidden */
37+
const urlMatcherSort = (a: MatcherUrlRule, b: MatcherUrlRule) =>
38+
!a.urlMatcher || !b.urlMatcher ? 0 : UrlMatcher.compare(a.urlMatcher, b.urlMatcher);
39+
40+
/** @hidden */
41+
const idSort = (a: UrlRule, b: UrlRule) => {
42+
// Identically sorted STATE and URLMATCHER best rule will be chosen by `matchPriority` after each rule matches the URL
43+
const useMatchPriority = { STATE: true, URLMATCHER: true };
44+
const equal = useMatchPriority[a.type] && useMatchPriority[b.type];
45+
return equal ? 0 : (a.$id || 0) - (b.$id || 0);
46+
};
2847

2948
/**
3049
* Default rule priority sorting function.
@@ -34,17 +53,25 @@ const getMatcher = prop("urlMatcher");
3453
* - Explicit priority (set rule priority using [[UrlRulesApi.when]])
3554
* - Rule type (STATE: 4, URLMATCHER: 4, REGEXP: 3, RAW: 2, OTHER: 1)
3655
* - `UrlMatcher` specificity ([[UrlMatcher.compare]]): works for STATE and URLMATCHER types to pick the most specific rule.
37-
* - Registration order (for rule types other than STATE and URLMATCHER)
56+
* - Rule registration order (for rule types other than STATE and URLMATCHER)
57+
* - Equally sorted State and UrlMatcher rules will each match the URL.
58+
* Then, the *best* match is chosen based on how many parameter values were matched.
3859
*
3960
* @coreapi
4061
*/
4162
let defaultRuleSortFn: (a: UrlRule, b: UrlRule) => number;
42-
defaultRuleSortFn = composeSort(
43-
sortBy(pipe(prop("priority"), x => -x)),
44-
sortBy(pipe(prop("type"), type => ({ "STATE": 4, "URLMATCHER": 4, "REGEXP": 3, "RAW": 2, "OTHER": 1 })[type])),
45-
(a, b) => (getMatcher(a) && getMatcher(b)) ? UrlMatcher.compare(getMatcher(a), getMatcher(b)) : 0,
46-
sortBy(prop("$id"), inArray([ "REGEXP", "RAW", "OTHER" ])),
47-
);
63+
defaultRuleSortFn = (a, b) => {
64+
let cmp = prioritySort(a, b);
65+
if (cmp !== 0) return cmp;
66+
67+
cmp = typeSort(a, b);
68+
if (cmp !== 0) return cmp;
69+
70+
cmp = urlMatcherSort(a as MatcherUrlRule, b as MatcherUrlRule);
71+
if (cmp !== 0) return cmp;
72+
73+
return idSort(a, b);
74+
};
4875

4976
/**
5077
* Updates URL and responds to URL changes
@@ -229,7 +256,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
229256
href(urlMatcher: UrlMatcher, params?: any, options?: { absolute: boolean }): string {
230257
let url = urlMatcher.format(params);
231258
if (url == null) return null;
232-
259+
233260
options = options || { absolute: false };
234261

235262
let cfg = this._router.urlService.config;

0 commit comments

Comments
 (0)