-
-
Notifications
You must be signed in to change notification settings - Fork 1k
feat(reg-exp-router): Introduced PreparedRegExpRouter #1796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
a7f8a88
feat(reg-exp-router): Introduced PreparedRegExpRouter
usualoma befcce4
fix: explicitly specify a type of `match` method.
usualoma e8dbcd2
test: refactor PreparedRegExpRouter test
usualoma 54b82ca
refactor: simplify PreparedRegExpRouter API.
usualoma e98a119
Merge branch 'main' into feat/prepared-reg-exp-router
usualoma f9eef28
fix(reg-exp-router): run `bun run format:fix`
usualoma 66146af
fix: applying updates from the main branch
usualoma 3634ad2
fix: merge relocateMap if path is already in the map
usualoma db5eb2a
fix: should not skip wildcard path
usualoma 48a4446
test(reg-exp-router): fix test for PreparedRegExpRouter
usualoma 2ebcf28
refactor(reg-exp-router): fix import statement order
usualoma 68e2279
refactor(reg-exp-router): in the first call, call the method directly…
usualoma 5353aaf
refactor(reg-exp-router): use for...in instead of Object.keys
usualoma 8b358c3
docs(reg-exp-router): add comments
usualoma 8c4e605
refactor(reg-exp-router): use a more explicit type instead of `as any`.
usualoma 2e32491
refactor(reg-exp-router): improve static route handling
usualoma b6d9068
refactor(reg-exp-router): throws an Error when an unregistered path i…
usualoma 55d147e
test(reg-exp-router): add test for PreparedRegExpRouter
usualoma 359780e
refactor(reg-exp-router): remove unnecessary guards as only METHOD_NA…
usualoma 1bbe9e0
ci: apply automated fixes
autofix-ci[bot] 4909093
fix(reg-exp-router): fix typo in test comment
usualoma 5965b7f
test(reg-exp-router): remove unnecessary type annotation in test
usualoma 1f9f94c
chore(benchmarks): update buildInitParams to use paths (spec change)
usualoma File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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,34 @@ | ||
| import type { ParamIndexMap, Result, Router } from '../../router' | ||
| import { METHOD_NAME_ALL } from '../../router' | ||
|
|
||
| export type HandlerData<T> = [T, ParamIndexMap][] | ||
| export type StaticMap<T> = Record<string, Result<T>> | ||
| export type Matcher<T> = [RegExp, HandlerData<T>[], StaticMap<T>] | ||
| export type MatcherMap<T> = Record<string, Matcher<T> | null> | ||
|
|
||
| export const emptyParam: string[] = [] | ||
| export const buildAllMatchersKey = Symbol('buildAllMatchers') | ||
| export function match<R extends Router<T>, T>(this: R, method: string, path: string): Result<T> { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const matchers: MatcherMap<T> = (this as any)[buildAllMatchersKey]() | ||
|
|
||
| const match = ((method, path) => { | ||
| const matcher = (matchers[method] || matchers[METHOD_NAME_ALL]) as Matcher<T> | ||
|
|
||
| const staticMatch = matcher[2][path] | ||
| if (staticMatch) { | ||
| return staticMatch | ||
| } | ||
|
|
||
| const match = path.match(matcher[0]) | ||
| if (!match) { | ||
| return [[], emptyParam] | ||
| } | ||
|
|
||
| const index = match.indexOf('', 1) | ||
| return [matcher[1][index], match] | ||
| }) as Router<T>['match'] | ||
|
|
||
| this.match = match | ||
| return match(method, path) | ||
| } |
This file contains hidden or 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,69 @@ | ||
| import { runTest } from '../common.case.test' | ||
| import { buildInitParams, serializeInitParams, PreparedRegExpRouter } from './prepared-router' | ||
|
|
||
| describe('PreparedRegExpRouter', async () => { | ||
| runTest({ | ||
| skip: [ | ||
| { | ||
| reason: 'UnsupportedPath', | ||
| tests: [ | ||
| 'Duplicate param name > parent', | ||
| 'Duplicate param name > child', | ||
| 'Capture Group > Complex capturing group > GET request', | ||
| 'Capture complex multiple directories > GET /part1/middle-b/latest', | ||
| 'Capture complex multiple directories > GET /part1/middle-b/end-c/latest', | ||
| 'Complex > Parameter with {.*} regexp', | ||
| ], | ||
| }, | ||
| { | ||
| reason: 'This route can not be added with `:label` to RegExpRouter. This is ambiguous', | ||
|
yusukebe marked this conversation as resolved.
Outdated
|
||
| tests: ['Including slashes > GET /js/main.js'], | ||
| }, | ||
| ], | ||
| newRouter: <T>() => { | ||
| let router: PreparedRegExpRouter<T> | ||
| const routes: [string, string, T][] = [] | ||
| return { | ||
| name: 'PreparedRegExpRouterBuilder', | ||
| add: (method: string, path: string, handler: T) => { | ||
| routes.push([method, path, handler]) | ||
| }, | ||
| match: (method: string, path: string) => { | ||
| if (!router) { | ||
| const serialized = serializeInitParams( | ||
| buildInitParams({ | ||
| paths: routes.map((r) => r[1]), | ||
| }) | ||
| ) | ||
| const params = eval(serialized) as ConstructorParameters<typeof PreparedRegExpRouter<T>> | ||
| router = new PreparedRegExpRouter<T>(...params) | ||
|
|
||
| for (const route of routes) { | ||
| router.add(...route) | ||
| } | ||
| } | ||
| return router.match(method, path) | ||
| }, | ||
| } | ||
| }, | ||
| }) | ||
|
|
||
| describe('add()', () => { | ||
| it('should add a route', () => { | ||
| const params = buildInitParams({ | ||
| paths: ['/hello'], | ||
| }) as ConstructorParameters<typeof PreparedRegExpRouter<string>> | ||
| const router = new PreparedRegExpRouter<string>(...params) | ||
| router.add('GET', '/hello', 'get hello') | ||
| expect(router.match('GET', '/hello')).toEqual([[['get hello', {}]], []]) | ||
| }) | ||
|
|
||
| it('should throw an error if the path is not pre-registered', () => { | ||
| const params = buildInitParams({ | ||
| paths: ['/hello'], | ||
| }) as ConstructorParameters<typeof PreparedRegExpRouter<string>> | ||
| const router = new PreparedRegExpRouter<string>(...params) | ||
| expect(() => router.add('GET', '/unknown', 'get hello')).toThrowError() | ||
| }) | ||
| }) | ||
| }) | ||
This file contains hidden or 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,141 @@ | ||
| import type { ParamIndexMap, Result, Router } from '../../router' | ||
| import { METHOD_NAME_ALL } from '../../router' | ||
| import type { HandlerData, Matcher, MatcherMap, StaticMap } from './matcher' | ||
| import { match, buildAllMatchersKey, emptyParam } from './matcher' | ||
| import { RegExpRouter } from './router' | ||
|
|
||
| type RelocateMap = Record<string, [(number | string)[], ParamIndexMap | undefined][]> | ||
|
|
||
| export class PreparedRegExpRouter<T> implements Router<T> { | ||
| name: string = 'PreparedRegExpRouter' | ||
| #matchers: MatcherMap<T> | ||
| #relocateMap: RelocateMap | ||
|
|
||
| constructor(matchers: MatcherMap<T>, relocateMap: RelocateMap) { | ||
| this.#matchers = matchers | ||
| this.#relocateMap = relocateMap | ||
| } | ||
|
|
||
| add(method: string, path: string, handler: T) { | ||
| const all = this.#matchers[METHOD_NAME_ALL] as Matcher<T> | ||
| this.#matchers[method] ||= [ | ||
| all[0], | ||
| all[1].map((list) => (Array.isArray(list) ? list.slice() : 0)) as HandlerData<T>[], | ||
| Object.keys(all[2]).reduce((obj, key) => { | ||
| obj[key] = [all[2][key][0].slice(), emptyParam] as Result<T> | ||
| return obj | ||
| }, {} as StaticMap<T>), | ||
| ] | ||
|
|
||
| if (path === '/*' || path === '*') { | ||
| const defaultHandlerData: [T, ParamIndexMap] = [handler, {}] | ||
| ;(method === METHOD_NAME_ALL ? Object.keys(this.#matchers) : [method]).forEach((m) => { | ||
| const matcher = this.#matchers[m] as Matcher<T> | ||
| matcher[1].forEach((list) => list && list.push(defaultHandlerData)) | ||
| Object.values(matcher[2]).forEach((list) => | ||
| (list[0] as [T, ParamIndexMap][]).push(defaultHandlerData) | ||
| ) | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| const data = this.#relocateMap[path] | ||
| if (!data) { | ||
| throw new Error(`Path ${path} is not registered`) | ||
| } | ||
| for (const [indexes, map] of data) { | ||
| ;(method === METHOD_NAME_ALL ? Object.keys(this.#matchers) : [method]).forEach((m) => { | ||
| const matcher = this.#matchers[m] as Matcher<T> | ||
| if (!map) { | ||
| // assumed to be a static route | ||
| matcher[2][path][0].push([handler, {}]) | ||
| } else { | ||
| indexes.forEach((index) => { | ||
| if (typeof index === 'number') { | ||
| matcher[1][index].push([handler, map]) | ||
| } else { | ||
| ;(matcher[2][index || path][0] as [T, ParamIndexMap][]).push([handler, map]) | ||
| } | ||
| }) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| [buildAllMatchersKey](): MatcherMap<T> { | ||
| return this.#matchers | ||
| } | ||
|
|
||
| match: typeof match<Router<T>, T> = match | ||
| } | ||
|
|
||
| export const buildInitParams: (params: { | ||
| paths: string[] | ||
| }) => ConstructorParameters<typeof PreparedRegExpRouter> = ({ paths }) => { | ||
| const router = new RegExpRouter<string>() | ||
| for (const path of paths) { | ||
| router.add(METHOD_NAME_ALL, path, path) | ||
| } | ||
|
|
||
| const matchers = router[buildAllMatchersKey]() | ||
| const all = matchers[METHOD_NAME_ALL] as Matcher<string> | ||
|
|
||
| const relocateMap: RelocateMap = {} | ||
| for (const path of paths) { | ||
| all[1].forEach((list, i) => { | ||
| list.forEach(([p, map]) => { | ||
| if (p === path) { | ||
| if (relocateMap[path]) { | ||
| relocateMap[path][0][1] = { | ||
| ...relocateMap[path][0][1], | ||
| ...map, | ||
| } | ||
| } else { | ||
| relocateMap[path] = [[[], map]] | ||
| } | ||
| if (relocateMap[path][0][0].findIndex((j) => j === i) === -1) { | ||
| relocateMap[path][0][0].push(i) | ||
| } | ||
| } | ||
| }) | ||
| }) | ||
| for (const path2 in all[2]) { | ||
| all[2][path2][0].forEach(([p]) => { | ||
| if (p === path) { | ||
| relocateMap[path] ||= [[[], undefined]] | ||
| const value = path2 === path ? '' : path2 | ||
| if (relocateMap[path][0][0].findIndex((v) => v === value) === -1) { | ||
| relocateMap[path][0][0].push(value) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| for (let i = 0, len = all[1].length; i < len; i++) { | ||
| all[1][i] = all[1][i] ? [] : (0 as unknown as HandlerData<string>) | ||
| } | ||
| for (const path in all[2]) { | ||
| all[2][path][0] = [] | ||
| } | ||
|
|
||
| return [matchers, relocateMap] | ||
| } | ||
|
|
||
| export const serializeInitParams: ( | ||
| params: ConstructorParameters<typeof PreparedRegExpRouter> | ||
| ) => string = ([matchers, relocateMap]) => { | ||
| for (const method in matchers) { | ||
| const matcher = matchers[method] as Matcher<string> | ||
| // escape the regular expression to serialize it with `JSON.stringify` | ||
| ;(matcher[0] as RegExp & { toJSON: () => string }).toJSON = function () { | ||
| return `@${this.toString()}@` | ||
| } | ||
| } | ||
| // unescape the regular expression so that it can be deserialized with `eval`. | ||
| const matchersStr = JSON.stringify(matchers).replace(/"@(.+?)@"/g, (_, str) => | ||
| str.replace(/\\\\/g, '\\') | ||
| ) | ||
| const relocateMapStr = JSON.stringify(relocateMap) | ||
| return `[${matchersStr},${relocateMapStr}]` | ||
| } |
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.