-
-
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(router): replace path-to-regexp with internal matcher (#64)
Closes #58
- Loading branch information
Showing
8 changed files
with
227 additions
and
47 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
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
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,133 @@ | ||
import { Route } from '../route'; | ||
import { matchRoute, parsePath } from './path-parser'; | ||
|
||
describe('parsePath', () => { | ||
it('should parse empty route', () => { | ||
expect(parsePath({ path: '', options: {} })).toEqual(/^[\/#\?]?$/i); | ||
expect(parsePath({ path: '/', options: {} })).toEqual(/^[\/#\?]?$/i); | ||
}); | ||
it('should parse empty wildcard route', () => { | ||
expect(parsePath({ path: '', options: { exact: false } })).toEqual( | ||
/^(?:[\/#\?](?=[]|$))?/i | ||
); | ||
expect(parsePath({ path: '/', options: { exact: false } })).toEqual( | ||
/^(?:[\/#\?](?=[]|$))?/i | ||
); | ||
}); | ||
it('should parse static route', () => { | ||
expect(parsePath({ path: 'first/second', options: {} })).toEqual( | ||
/^\/first\/second[\/#\?]?$/i | ||
); | ||
expect(parsePath({ path: '/first/second', options: {} })).toEqual( | ||
/^\/first\/second[\/#\?]?$/i | ||
); | ||
}); | ||
it('should remove ending slash', () => { | ||
expect(parsePath({ path: 'first/', options: {} })).toEqual( | ||
/^\/first[\/#\?]?$/i | ||
); | ||
expect(parsePath({ path: '/first/', options: {} })).toEqual( | ||
/^\/first[\/#\?]?$/i | ||
); | ||
}); | ||
it('should parse static wildcard route', () => { | ||
expect( | ||
parsePath({ path: 'first/second', options: { exact: false } }) | ||
).toEqual(/^\/first\/second(?:[\/#\?](?=[]|$))?(?=[\/#\?]|[]|$)/i); | ||
expect( | ||
parsePath({ path: '/first/second', options: { exact: false } }) | ||
).toEqual(/^\/first\/second(?:[\/#\?](?=[]|$))?(?=[\/#\?]|[]|$)/i); | ||
}); | ||
|
||
it('should parse dynamic route', () => { | ||
expect(parsePath({ path: ':id', options: {} })).toEqual( | ||
/^(?:\/([^\/#\?]+?))[\/#\?]?$/i | ||
); | ||
expect(parsePath({ path: '/books/:bookId', options: {} })).toEqual( | ||
/^\/books(?:\/([^\/#\?]+?))[\/#\?]?$/i | ||
); | ||
}); | ||
|
||
it('should parse dynamic wildcard route', () => { | ||
expect(parsePath({ path: ':id', options: { exact: false } })).toEqual( | ||
/^(?:\/([^\/#\?]+?))(?:[\/#\?](?=[]|$))?(?=[\/#\?]|[]|$)/i | ||
); | ||
expect( | ||
parsePath({ path: '/books/:bookId', options: { exact: false } }) | ||
).toEqual( | ||
/^\/books(?:\/([^\/#\?]+?))(?:[\/#\?](?=[]|$))?(?=[\/#\?]|[]|$)/i | ||
); | ||
}); | ||
}); | ||
|
||
describe('matchRoute', () => { | ||
it('should match wildcard route', () => { | ||
const route: Route = { path: '', options: { exact: false } }; | ||
route.matcher = parsePath(route); | ||
|
||
expect(matchRoute('/', route)).toEqual({ path: '/', params: {} }); | ||
expect(matchRoute('/first', route)).toEqual({ path: '', params: {} }); | ||
expect(matchRoute('/first/second/third', route)).toEqual({ | ||
path: '', | ||
params: {}, | ||
}); | ||
}); | ||
it('should match empty route', () => { | ||
const route: Route = { path: '', options: {} }; | ||
route.matcher = parsePath(route); | ||
|
||
expect(matchRoute('/', route)).toEqual({ path: '/', params: {} }); | ||
expect(matchRoute('/first', route)).not.toBeDefined(); | ||
expect(matchRoute('/first/second', route)).not.toBeDefined(); | ||
}); | ||
it('should match static wildcard route', () => { | ||
const route: Route = { path: 'first/second', options: { exact: false } }; | ||
route.matcher = parsePath(route); | ||
|
||
expect(matchRoute('/first/second', route)).toEqual({ | ||
path: '/first/second', | ||
params: {}, | ||
}); | ||
expect(matchRoute('/first', route)).not.toBeDefined(); | ||
expect(matchRoute('/first/second/third', route)).toEqual({ | ||
path: '/first/second', | ||
params: {}, | ||
}); | ||
}); | ||
it('should match static route', () => { | ||
const route: Route = { path: 'first/second', options: {} }; | ||
route.matcher = parsePath(route); | ||
|
||
expect(matchRoute('/first/second', route)).toEqual({ | ||
path: '/first/second', | ||
params: {}, | ||
}); | ||
expect(matchRoute('/first', route)).not.toBeDefined(); | ||
expect(matchRoute('/first/second/third', route)).not.toBeDefined(); | ||
}); | ||
it('should match dynamic wildcard route', () => { | ||
const route: Route = { path: 'first/:id', options: { exact: false } }; | ||
route.matcher = parsePath(route); | ||
|
||
expect(matchRoute('/first/second', route)).toEqual({ | ||
path: '/first/second', | ||
params: { id: 'second' }, | ||
}); | ||
expect(matchRoute('/first', route)).not.toBeDefined(); | ||
expect(matchRoute('/first/second/third', route)).toEqual({ | ||
path: '/first/second', | ||
params: { id: 'second' }, | ||
}); | ||
}); | ||
it('should match dynamic route', () => { | ||
const route: Route = { path: 'first/:id/:name', options: {} }; | ||
route.matcher = parsePath(route); | ||
|
||
expect(matchRoute('/first/second', route)).not.toBeDefined(); | ||
expect(matchRoute('/first', route)).not.toBeDefined(); | ||
expect(matchRoute('/first/second/third', route)).toEqual({ | ||
path: '/first/second/third', | ||
params: { id: 'second', name: 'third' }, | ||
}); | ||
}); | ||
}); |
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,63 @@ | ||
import { Route } from '../route'; | ||
import { Params } from '../route-params.service'; | ||
|
||
const PARAM_PREFIX = ':'; | ||
|
||
export interface RouteMatch { | ||
path: string; | ||
params: Params; | ||
} | ||
|
||
const DIV = '\\/'; // / | ||
const DIV_PARAM = `(?:${DIV}([^\\/#\\?]+?))`; // capturing group for one or more of not (/, # or ?), optional (TODO: check if optional is needed) | ||
const PATH_END = '[\\/#\\?]'; // path end: /, # or ? | ||
const END = '[]|$'; // null or end | ||
const EXACT_END = `${PATH_END}?$`; // match PATH_END optionally and END | ||
const WILDCARD = `(?:${PATH_END}(?=${END}))?`; // match optionally PATH_END followed by END | ||
const NON_EXACT_END = `${WILDCARD}(?=${PATH_END}|${END})`; // match WILDCARD followed by PATH_END or END | ||
|
||
export function getPathSegments(route: Route): string[] { | ||
const sanitizedPath = route.path.replace(/^\//, '').replace(/(?:\/$)/, ''); | ||
return sanitizedPath ? sanitizedPath.split('/') : []; | ||
} | ||
|
||
export const parsePath = (route: Route): RegExp => { | ||
const segments = getPathSegments(route); | ||
const regexBody = segments.reduce( | ||
(acc, segment) => | ||
segment.startsWith(PARAM_PREFIX) | ||
? `${acc}${DIV_PARAM}` | ||
: `${acc}${DIV}${segment}`, | ||
'' | ||
); | ||
|
||
if (route.options.exact ?? true) { | ||
return new RegExp(`^${regexBody}${EXACT_END}`, 'i'); | ||
} else { | ||
return new RegExp( | ||
`^${regexBody}${regexBody ? NON_EXACT_END : WILDCARD}`, | ||
'i' | ||
); | ||
} | ||
}; | ||
|
||
export const matchRoute = ( | ||
url: string, | ||
route: Route | ||
): RouteMatch | undefined => { | ||
const match = route.matcher?.exec(url); | ||
if (!match) { | ||
return; | ||
} | ||
const keys = getPathSegments(route) | ||
.filter((s) => s.startsWith(PARAM_PREFIX)) | ||
.map((s) => s.slice(1)); | ||
|
||
return { | ||
path: match[0], | ||
params: keys.reduce( | ||
(acc, key, index) => ({ ...acc, [key]: match[index + 1] }), | ||
{} | ||
), | ||
}; | ||
}; |
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 |
---|---|---|
|
@@ -8872,11 +8872,6 @@ [email protected]: | |
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" | ||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= | ||
|
||
path-to-regexp@^6.1.0: | ||
version "6.1.0" | ||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427" | ||
integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw== | ||
|
||
path-type@^1.0.0: | ||
version "1.1.0" | ||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" | ||
|