-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: sort routes based on path and matching priority
Closes #48
- Loading branch information
Showing
3 changed files
with
148 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(':'); | ||
} |