Skip to content

Commit 1ca2a13

Browse files
committed
refactor: simplify matcher interfaces
1 parent d462758 commit 1ca2a13

File tree

6 files changed

+438
-165
lines changed

6 files changed

+438
-165
lines changed

packages/router/src/new-route-resolver/matcher-location.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import type { MatcherName } from './matcher'
66
*/
77
export type MatcherParamsFormatted = Record<string, unknown>
88

9+
/**
10+
* Empty object in TS.
11+
*/
12+
export type EmptyParams = Record<PropertyKey, never>
13+
914
export interface MatcherLocationAsNamed {
1015
name: MatcherName
16+
// FIXME: should this be optional?
1117
params: MatcherParamsFormatted
1218
query?: LocationQueryRaw
1319
hash?: string

packages/router/src/new-route-resolver/matcher.spec.ts

Lines changed: 132 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,136 @@
11
import { describe, expect, it } from 'vitest'
2-
import { MatcherPatternImpl, MatcherPatternPath } from './matcher-pattern'
3-
import { createCompiledMatcher } from './matcher'
2+
import { MatcherPatternImpl } from './matcher-pattern'
3+
import { createCompiledMatcher, NO_MATCH_LOCATION } from './matcher'
4+
import {
5+
MatcherPatternParams_Base,
6+
MatcherPattern,
7+
MatcherPatternPath,
8+
MatcherPatternQuery,
9+
} from './new-matcher-pattern'
10+
import { miss } from './matchers/errors'
11+
import { EmptyParams } from './matcher-location'
412

513
function createMatcherPattern(
614
...args: ConstructorParameters<typeof MatcherPatternImpl>
715
) {
816
return new MatcherPatternImpl(...args)
917
}
1018

11-
const EMPTY_PATH_PATTERN_MATCHER = {
12-
match: (path: string) => ({}),
13-
parse: (params: {}) => ({}),
14-
serialize: (params: {}) => ({}),
15-
buildPath: () => '/',
16-
} satisfies MatcherPatternPath
19+
const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string }> = {
20+
match(path) {
21+
return { pathMatch: path }
22+
},
23+
build({ pathMatch }) {
24+
return pathMatch
25+
},
26+
}
27+
28+
const EMPTY_PATH_PATTERN_MATCHER: MatcherPatternPath<EmptyParams> = {
29+
match: path => {
30+
if (path !== '/') {
31+
throw miss()
32+
}
33+
return {}
34+
},
35+
build: () => '/',
36+
}
37+
38+
const USER_ID_PATH_PATTERN_MATCHER: MatcherPatternPath<{ id: number }> = {
39+
match(value) {
40+
const match = value.match(/^\/users\/(\d+)$/)
41+
if (!match?.[1]) {
42+
throw miss()
43+
}
44+
const id = Number(match[1])
45+
if (Number.isNaN(id)) {
46+
throw miss()
47+
}
48+
return { id }
49+
},
50+
build({ id }) {
51+
return `/users/${id}`
52+
},
53+
}
54+
55+
const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = {
56+
match: query => {
57+
const page = Number(query.page)
58+
return {
59+
page: Number.isNaN(page) ? 1 : page,
60+
}
61+
},
62+
build: params => ({ page: String(params.page) }),
63+
} satisfies MatcherPatternQuery<{ page: number }>
64+
65+
const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base<
66+
string,
67+
{ hash: string | null }
68+
> = {
69+
match: hash => ({ hash: hash ? hash.slice(1) : null }),
70+
build: ({ hash }) => (hash ? `#${hash}` : ''),
71+
}
72+
73+
const EMPTY_PATH_ROUTE = {
74+
name: 'no params',
75+
path: EMPTY_PATH_PATTERN_MATCHER,
76+
} satisfies MatcherPattern
77+
78+
const USER_ID_ROUTE = {
79+
name: 'user-id',
80+
path: USER_ID_PATH_PATTERN_MATCHER,
81+
} satisfies MatcherPattern
1782

1883
describe('Matcher', () => {
84+
describe('adding and removing', () => {
85+
it('add static path', () => {
86+
const matcher = createCompiledMatcher()
87+
matcher.addRoute(EMPTY_PATH_ROUTE)
88+
})
89+
90+
it('adds dynamic path', () => {
91+
const matcher = createCompiledMatcher()
92+
matcher.addRoute(USER_ID_ROUTE)
93+
})
94+
})
95+
1996
describe('resolve()', () => {
2097
describe('absolute locationss as strings', () => {
2198
it('resolves string locations with no params', () => {
2299
const matcher = createCompiledMatcher()
23-
matcher.addRoute(
24-
createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER)
25-
)
100+
matcher.addRoute(EMPTY_PATH_ROUTE)
26101

27-
expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({
28-
path: '/foo',
102+
expect(matcher.resolve('/?a=a&b=b#h')).toMatchObject({
103+
path: '/',
29104
params: {},
30105
query: { a: 'a', b: 'b' },
31106
hash: '#h',
32107
})
33108
})
34109

110+
it('resolves a not found string', () => {
111+
const matcher = createCompiledMatcher()
112+
expect(matcher.resolve('/bar?q=1#hash')).toEqual({
113+
...NO_MATCH_LOCATION,
114+
fullPath: '/bar?q=1#hash',
115+
path: '/bar',
116+
query: { q: '1' },
117+
hash: '#hash',
118+
matched: [],
119+
})
120+
})
121+
35122
it('resolves string locations with params', () => {
36123
const matcher = createCompiledMatcher()
37-
matcher.addRoute(
38-
// /users/:id
39-
createMatcherPattern(Symbol('foo'), {
40-
match: (path: string) => {
41-
const match = path.match(/^\/foo\/([^/]+?)$/)
42-
if (!match) throw new Error('no match')
43-
return { id: match[1] }
44-
},
45-
parse: (params: { id: string }) => ({ id: Number(params.id) }),
46-
serialize: (params: { id: number }) => ({ id: String(params.id) }),
47-
buildPath: params => `/foo/${params.id}`,
48-
})
49-
)
50-
51-
expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({
52-
path: '/foo/1',
124+
matcher.addRoute(USER_ID_ROUTE)
125+
126+
expect(matcher.resolve('/users/1?a=a&b=b#h')).toMatchObject({
127+
path: '/users/1',
53128
params: { id: 1 },
54129
query: { a: 'a', b: 'b' },
55130
hash: '#h',
56131
})
57-
expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({
58-
path: '/foo/54',
132+
expect(matcher.resolve('/users/54?a=a&b=b#h')).toMatchObject({
133+
path: '/users/54',
59134
params: { id: 54 },
60135
query: { a: 'a', b: 'b' },
61136
hash: '#h',
@@ -64,21 +139,16 @@ describe('Matcher', () => {
64139

65140
it('resolve string locations with query', () => {
66141
const matcher = createCompiledMatcher()
67-
matcher.addRoute(
68-
createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, {
69-
match: query => ({
70-
id: Array.isArray(query.id) ? query.id[0] : query.id,
71-
}),
72-
parse: (params: { id: string }) => ({ id: Number(params.id) }),
73-
serialize: (params: { id: number }) => ({ id: String(params.id) }),
74-
})
75-
)
76-
77-
expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({
78-
params: { id: 100 },
142+
matcher.addRoute({
143+
path: ANY_PATH_PATTERN_MATCHER,
144+
query: PAGE_QUERY_PATTERN_MATCHER,
145+
})
146+
147+
expect(matcher.resolve('/foo?page=100&b=b#h')).toMatchObject({
148+
params: { page: 100 },
79149
path: '/foo',
80150
query: {
81-
id: '100',
151+
page: '100',
82152
b: 'b',
83153
},
84154
hash: '#h',
@@ -87,94 +157,37 @@ describe('Matcher', () => {
87157

88158
it('resolves string locations with hash', () => {
89159
const matcher = createCompiledMatcher()
90-
matcher.addRoute(
91-
createMatcherPattern(
92-
Symbol('foo'),
93-
EMPTY_PATH_PATTERN_MATCHER,
94-
undefined,
95-
{
96-
match: hash => hash,
97-
parse: hash => ({ a: hash.slice(1) }),
98-
serialize: ({ a }) => '#a',
99-
}
100-
)
101-
)
160+
matcher.addRoute({
161+
path: ANY_PATH_PATTERN_MATCHER,
162+
hash: ANY_HASH_PATTERN_MATCHER,
163+
})
102164

103165
expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({
104166
hash: '#bar',
105-
params: { a: 'bar' },
167+
params: { hash: 'bar' },
106168
path: '/foo',
107169
query: { a: 'a', b: 'b' },
108170
})
109171
})
110172

111-
it('returns a valid location with an empty `matched` array if no match', () => {
173+
it('combines path, query and hash params', () => {
112174
const matcher = createCompiledMatcher()
113-
expect(matcher.resolve('/bar')).toMatchInlineSnapshot(
114-
{
115-
hash: '',
116-
matched: [],
117-
params: {},
118-
path: '/bar',
119-
query: {},
120-
},
121-
`
122-
{
123-
"fullPath": "/bar",
124-
"hash": "",
125-
"matched": [],
126-
"name": Symbol(no-match),
127-
"params": {},
128-
"path": "/bar",
129-
"query": {},
130-
}
131-
`
132-
)
133-
})
175+
matcher.addRoute({
176+
path: USER_ID_PATH_PATTERN_MATCHER,
177+
query: PAGE_QUERY_PATTERN_MATCHER,
178+
hash: ANY_HASH_PATTERN_MATCHER,
179+
})
134180

135-
it('resolves string locations with all', () => {
136-
const matcher = createCompiledMatcher()
137-
matcher.addRoute(
138-
createMatcherPattern(
139-
Symbol('foo'),
140-
{
141-
buildPath: params => `/foo/${params.id}`,
142-
match: path => {
143-
const match = path.match(/^\/foo\/([^/]+?)$/)
144-
if (!match) throw new Error('no match')
145-
return { id: match[1] }
146-
},
147-
parse: params => ({ id: Number(params.id) }),
148-
serialize: params => ({ id: String(params.id) }),
149-
},
150-
{
151-
match: query => ({
152-
id: Array.isArray(query.id) ? query.id[0] : query.id,
153-
}),
154-
parse: params => ({ q: Number(params.id) }),
155-
serialize: params => ({ id: String(params.q) }),
156-
},
157-
{
158-
match: hash => hash,
159-
parse: hash => ({ a: hash.slice(1) }),
160-
serialize: ({ a }) => '#a',
161-
}
162-
)
163-
)
164-
165-
expect(matcher.resolve('/foo/1?id=100#bar')).toMatchObject({
166-
hash: '#bar',
167-
params: { id: 1, q: 100, a: 'bar' },
181+
expect(matcher.resolve('/users/24?page=100#bar')).toMatchObject({
182+
params: { id: 24, page: 100, hash: 'bar' },
168183
})
169184
})
170185
})
171186

172187
describe('relative locations as strings', () => {
173188
it('resolves a simple relative location', () => {
174189
const matcher = createCompiledMatcher()
175-
matcher.addRoute(
176-
createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER)
177-
)
190+
matcher.addRoute({ path: ANY_PATH_PATTERN_MATCHER })
178191

179192
expect(
180193
matcher.resolve('foo', matcher.resolve('/nested/'))
@@ -206,9 +219,10 @@ describe('Matcher', () => {
206219
describe('named locations', () => {
207220
it('resolves named locations with no params', () => {
208221
const matcher = createCompiledMatcher()
209-
matcher.addRoute(
210-
createMatcherPattern('home', EMPTY_PATH_PATTERN_MATCHER)
211-
)
222+
matcher.addRoute({
223+
name: 'home',
224+
path: EMPTY_PATH_PATTERN_MATCHER,
225+
})
212226

213227
expect(matcher.resolve({ name: 'home', params: {} })).toMatchObject({
214228
name: 'home',

packages/router/src/new-route-resolver/matcher.test-d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expectTypeOf, it } from 'vitest'
2-
import { NEW_LocationResolved, createCompiledMatcher } from './matcher'
2+
import { NEW_LocationResolved, RouteResolver } from './matcher'
33

44
describe('Matcher', () => {
5-
const matcher = createCompiledMatcher()
5+
const matcher: RouteResolver<unknown, unknown> = {} as any
66

77
describe('matcher.resolve()', () => {
88
it('resolves absolute string locations', () => {

0 commit comments

Comments
 (0)