Skip to content

Commit

Permalink
feat: sort routes based on path and matching priority
Browse files Browse the repository at this point in the history
Closes #48
  • Loading branch information
meeroslav committed Nov 14, 2020
1 parent e79a740 commit 00564b4
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 6 deletions.
9 changes: 3 additions & 6 deletions libs/angular-routing/src/lib/router.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { pathToRegexp, match } from 'path-to-regexp';
import { Route, ActiveRoute } from './route';
import { Router } from './router.service';
import { compareParams, Params } from './route-params.service';
import { compareRoutes } from './utils/compare-routes';

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

private _routes$ = new BehaviorSubject<Route[]>([]);
readonly routes$ = this._routes$.pipe(
scan((routes, route) => {
routes = routes.concat(route);

return routes;
})
scan((routes, route) => routes.concat(route).sort(compareRoutes))
);

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

route.matcher = route.matcher || routeRegex;
Expand Down
71 changes: 71 additions & 0 deletions libs/angular-routing/src/lib/utils/compare-routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { pathToRegexp } from 'path-to-regexp';
import { Route } from '../route';
import { compareRoutes } from './compare-routes';

describe('compareRoutes', () => {
it('should return 0 if matchers are same', () => {
const a = makeRoute({ path: '/', options: {} });
const b = makeRoute({ path: '/', options: { exact: true } });
expect(compareRoutes(a, b)).toEqual(0);
});

it('should ignore names of params when comparing', () => {
const a = makeRoute({ path: '/:user', options: {} });
const b = makeRoute({ path: '/:person', options: {} });
expect(compareRoutes(a, b)).toEqual(0);
});

it('should prioritize root route over wildcard route', () => {
const a = makeRoute({ path: '/', options: { exact: false } });
const b = makeRoute({ path: '', options: {} });

expect(compareRoutes(a, b)).toEqual(1);
});

it('should prioritize non-empty path over empty', () => {
const a = makeRoute({ path: '/', options: { exact: true } });
const b = makeRoute({ path: 'test', options: {} });

expect(compareRoutes(a, b)).toEqual(1);
});

it('should prioritize exact route over non-exact', () => {
const a = makeRoute({ path: 'test', options: {} });
const b = makeRoute({ path: 'test', options: { exact: false } });

expect(compareRoutes(a, b)).toEqual(-1);
});

it('should prioritize static over parametrized paths', () => {
const a = makeRoute({ path: '/test/:param', options: {} });
const b = makeRoute({ path: '/test/static', options: {} });
const c = makeRoute({ path: '/:param/test', options: {} });

expect(compareRoutes(a, b)).toEqual(1);
expect(compareRoutes(a, c)).toEqual(-1);
expect(compareRoutes(b, c)).toEqual(-1);
});
it('should prioritize longer paths', () => {
const a = makeRoute({ path: '/test/:param', options: { exact: true } });
const b = makeRoute({ path: '/test/:param/user/:id', options: {} });
const c = makeRoute({
path: '/test/:param/user',
options: { exact: true },
});

expect(compareRoutes(a, b)).toEqual(1);
expect(compareRoutes(a, c)).toEqual(1);
expect(compareRoutes(b, c)).toEqual(-1);
});
});

function makeRoute(route: Route): Route {
route.matcher = pathToRegexp(normalizePath(route), [], {
end: route.options.exact ?? true,
});
return route;
}

function normalizePath(route: Route): string {
return route.path.startsWith('/') ? route.path : `/${route.path}`;
}
74 changes: 74 additions & 0 deletions libs/angular-routing/src/lib/utils/compare-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Route } from '../route';

/**
* Compares two routes and returns sorting number
* 0 - equal
* -1 - `a` has priority over `b`
* 1 - `b` has priority over `a`
*
* @param a Route
* @param b Route
*/
export const compareRoutes = (a: Route, b: Route): number => {
// as matchers combine normalized path and `exact` option it's safe to compare regexps
if (a.matcher.toString() === b.matcher.toString()) {
return 0;
}
const aSegments = getPathSegments(a);
const bSegments = getPathSegments(b);

for (let i = 0; i < Math.max(aSegments.length, bSegments.length); i++) {
const current = compareSegments(aSegments, bSegments, i);
if (current) {
return current;
}
}
// when paths are same, exact has priority
return a.options.exact ?? true ? -1 : 1;
};

function getPathSegments(route: Route): string[] {
return route.path.replace(/^\//, '').split('/');
}

function compareSegments(
aSegments: string[],
bSegments: string[],
index: number
): number {
// if a has no more segments -> return 1
if (aSegments.length <= index) {
return 1;
}
// if b has no more segments -> return -1
if (bSegments.length <= index) {
return -1;
}
if (aSegments[index] === bSegments[index]) {
return 0;
}
// prioritize non-empty path over empty
if (!aSegments[index]) {
return 1;
}
if (!bSegments[index]) {
return -1;
}
// ignore param names
if (isParam(aSegments[index]) && isParam(bSegments[index])) {
return 0;
}
// static segment has priority over param
if (isParam(aSegments[index])) {
return 1;
}
if (isParam(bSegments[index])) {
return -1;
}
// when all is same run string comparison
return aSegments[index].localeCompare(bSegments[index]);
}

function isParam(segment: string): boolean {
return segment.startsWith(':');
}

0 comments on commit 00564b4

Please sign in to comment.