Skip to content

Commit 00564b4

Browse files
authored
feat: sort routes based on path and matching priority
Closes #48
1 parent e79a740 commit 00564b4

File tree

3 files changed

+148
-6
lines changed

3 files changed

+148
-6
lines changed

libs/angular-routing/src/lib/router.component.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { pathToRegexp, match } from 'path-to-regexp';
2020
import { Route, ActiveRoute } from './route';
2121
import { Router } from './router.service';
2222
import { compareParams, Params } from './route-params.service';
23+
import { compareRoutes } from './utils/compare-routes';
2324

2425
@Component({
2526
// tslint:disable-next-line:component-selector
@@ -36,11 +37,7 @@ export class RouterComponent implements OnInit, OnDestroy {
3637

3738
private _routes$ = new BehaviorSubject<Route[]>([]);
3839
readonly routes$ = this._routes$.pipe(
39-
scan((routes, route) => {
40-
routes = routes.concat(route);
41-
42-
return routes;
43-
})
40+
scan((routes, route) => routes.concat(route).sort(compareRoutes))
4441
);
4542

4643
public basePath = '';
@@ -107,7 +104,7 @@ export class RouterComponent implements OnInit, OnDestroy {
107104
registerRoute(route: Route) {
108105
const normalized = this.normalizePath(route.path);
109106
const routeRegex = pathToRegexp(normalized, [], {
110-
end: route.options.exact,
107+
end: route.options.exact ?? true,
111108
});
112109

113110
route.matcher = route.matcher || routeRegex;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { pathToRegexp } from 'path-to-regexp';
2+
import { Route } from '../route';
3+
import { compareRoutes } from './compare-routes';
4+
5+
describe('compareRoutes', () => {
6+
it('should return 0 if matchers are same', () => {
7+
const a = makeRoute({ path: '/', options: {} });
8+
const b = makeRoute({ path: '/', options: { exact: true } });
9+
expect(compareRoutes(a, b)).toEqual(0);
10+
});
11+
12+
it('should ignore names of params when comparing', () => {
13+
const a = makeRoute({ path: '/:user', options: {} });
14+
const b = makeRoute({ path: '/:person', options: {} });
15+
expect(compareRoutes(a, b)).toEqual(0);
16+
});
17+
18+
it('should prioritize root route over wildcard route', () => {
19+
const a = makeRoute({ path: '/', options: { exact: false } });
20+
const b = makeRoute({ path: '', options: {} });
21+
22+
expect(compareRoutes(a, b)).toEqual(1);
23+
});
24+
25+
it('should prioritize non-empty path over empty', () => {
26+
const a = makeRoute({ path: '/', options: { exact: true } });
27+
const b = makeRoute({ path: 'test', options: {} });
28+
29+
expect(compareRoutes(a, b)).toEqual(1);
30+
});
31+
32+
it('should prioritize exact route over non-exact', () => {
33+
const a = makeRoute({ path: 'test', options: {} });
34+
const b = makeRoute({ path: 'test', options: { exact: false } });
35+
36+
expect(compareRoutes(a, b)).toEqual(-1);
37+
});
38+
39+
it('should prioritize static over parametrized paths', () => {
40+
const a = makeRoute({ path: '/test/:param', options: {} });
41+
const b = makeRoute({ path: '/test/static', options: {} });
42+
const c = makeRoute({ path: '/:param/test', options: {} });
43+
44+
expect(compareRoutes(a, b)).toEqual(1);
45+
expect(compareRoutes(a, c)).toEqual(-1);
46+
expect(compareRoutes(b, c)).toEqual(-1);
47+
});
48+
it('should prioritize longer paths', () => {
49+
const a = makeRoute({ path: '/test/:param', options: { exact: true } });
50+
const b = makeRoute({ path: '/test/:param/user/:id', options: {} });
51+
const c = makeRoute({
52+
path: '/test/:param/user',
53+
options: { exact: true },
54+
});
55+
56+
expect(compareRoutes(a, b)).toEqual(1);
57+
expect(compareRoutes(a, c)).toEqual(1);
58+
expect(compareRoutes(b, c)).toEqual(-1);
59+
});
60+
});
61+
62+
function makeRoute(route: Route): Route {
63+
route.matcher = pathToRegexp(normalizePath(route), [], {
64+
end: route.options.exact ?? true,
65+
});
66+
return route;
67+
}
68+
69+
function normalizePath(route: Route): string {
70+
return route.path.startsWith('/') ? route.path : `/${route.path}`;
71+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Route } from '../route';
2+
3+
/**
4+
* Compares two routes and returns sorting number
5+
* 0 - equal
6+
* -1 - `a` has priority over `b`
7+
* 1 - `b` has priority over `a`
8+
*
9+
* @param a Route
10+
* @param b Route
11+
*/
12+
export const compareRoutes = (a: Route, b: Route): number => {
13+
// as matchers combine normalized path and `exact` option it's safe to compare regexps
14+
if (a.matcher.toString() === b.matcher.toString()) {
15+
return 0;
16+
}
17+
const aSegments = getPathSegments(a);
18+
const bSegments = getPathSegments(b);
19+
20+
for (let i = 0; i < Math.max(aSegments.length, bSegments.length); i++) {
21+
const current = compareSegments(aSegments, bSegments, i);
22+
if (current) {
23+
return current;
24+
}
25+
}
26+
// when paths are same, exact has priority
27+
return a.options.exact ?? true ? -1 : 1;
28+
};
29+
30+
function getPathSegments(route: Route): string[] {
31+
return route.path.replace(/^\//, '').split('/');
32+
}
33+
34+
function compareSegments(
35+
aSegments: string[],
36+
bSegments: string[],
37+
index: number
38+
): number {
39+
// if a has no more segments -> return 1
40+
if (aSegments.length <= index) {
41+
return 1;
42+
}
43+
// if b has no more segments -> return -1
44+
if (bSegments.length <= index) {
45+
return -1;
46+
}
47+
if (aSegments[index] === bSegments[index]) {
48+
return 0;
49+
}
50+
// prioritize non-empty path over empty
51+
if (!aSegments[index]) {
52+
return 1;
53+
}
54+
if (!bSegments[index]) {
55+
return -1;
56+
}
57+
// ignore param names
58+
if (isParam(aSegments[index]) && isParam(bSegments[index])) {
59+
return 0;
60+
}
61+
// static segment has priority over param
62+
if (isParam(aSegments[index])) {
63+
return 1;
64+
}
65+
if (isParam(bSegments[index])) {
66+
return -1;
67+
}
68+
// when all is same run string comparison
69+
return aSegments[index].localeCompare(bSegments[index]);
70+
}
71+
72+
function isParam(segment: string): boolean {
73+
return segment.startsWith(':');
74+
}

0 commit comments

Comments
 (0)