Skip to content

Commit d9c63f1

Browse files
committed
refactor: improve param parser types to also parse null and accept a raw type
1 parent 25e4e7d commit d9c63f1

File tree

11 files changed

+376
-489
lines changed

11 files changed

+376
-489
lines changed

packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts

Lines changed: 21 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,6 @@ describe('MatcherPatternQueryParam', () => {
4040
)
4141
expect(matcher.match({ user_id: null })).toEqual({ userId: null })
4242
})
43-
44-
it('handles missing query param', () => {
45-
const matcher = new MatcherPatternQueryParam(
46-
'userId',
47-
'user_id',
48-
'value',
49-
PARAM_PARSER_DEFAULTS
50-
)
51-
expect(matcher.match({})).toEqual({
52-
userId: undefined,
53-
})
54-
})
5543
})
5644

5745
describe('match() - format: array', () => {
@@ -98,50 +86,6 @@ describe('MatcherPatternQueryParam', () => {
9886
})
9987
})
10088

101-
describe('match() - format: both', () => {
102-
it('preserves single string value', () => {
103-
const matcher = new MatcherPatternQueryParam(
104-
'data',
105-
'value',
106-
'both',
107-
PARAM_PARSER_DEFAULTS
108-
)
109-
expect(matcher.match({ value: 'single' })).toEqual({ data: 'single' })
110-
})
111-
112-
it('preserves array value', () => {
113-
const matcher = new MatcherPatternQueryParam(
114-
'data',
115-
'values',
116-
'both',
117-
PARAM_PARSER_DEFAULTS
118-
)
119-
expect(matcher.match({ values: ['a', 'b'] })).toEqual({
120-
data: ['a', 'b'],
121-
})
122-
})
123-
124-
it('preserves null', () => {
125-
const matcher = new MatcherPatternQueryParam(
126-
'data',
127-
'value',
128-
'both',
129-
PARAM_PARSER_DEFAULTS
130-
)
131-
expect(matcher.match({ value: null })).toEqual({ data: null })
132-
})
133-
134-
it('handles missing query param', () => {
135-
const matcher = new MatcherPatternQueryParam(
136-
'data',
137-
'value',
138-
'both',
139-
PARAM_PARSER_DEFAULTS
140-
)
141-
expect(matcher.match({})).toEqual({ data: undefined })
142-
})
143-
})
144-
14589
describe('build()', () => {
14690
describe('format: value', () => {
14791
it('builds query from single value', () => {
@@ -203,40 +147,6 @@ describe('MatcherPatternQueryParam', () => {
203147
expect(matcher.build({ tags: ['vue'] })).toEqual({ tag: ['vue'] })
204148
})
205149
})
206-
207-
describe('format: both', () => {
208-
it('builds query from single value', () => {
209-
const matcher = new MatcherPatternQueryParam(
210-
'data',
211-
'value',
212-
'both',
213-
PARAM_PARSER_DEFAULTS
214-
)
215-
expect(matcher.build({ data: 'single' })).toEqual({ value: 'single' })
216-
})
217-
218-
it('builds query from array value', () => {
219-
const matcher = new MatcherPatternQueryParam(
220-
'data',
221-
'values',
222-
'both',
223-
PARAM_PARSER_DEFAULTS
224-
)
225-
expect(matcher.build({ data: ['a', 'b'] })).toEqual({
226-
values: ['a', 'b'],
227-
})
228-
})
229-
230-
it('builds query from null value', () => {
231-
const matcher = new MatcherPatternQueryParam(
232-
'data',
233-
'value',
234-
'both',
235-
PARAM_PARSER_DEFAULTS
236-
)
237-
expect(matcher.build({ data: null })).toEqual({ value: null })
238-
})
239-
})
240150
})
241151

242152
describe('default values', () => {
@@ -323,14 +233,27 @@ describe('MatcherPatternQueryParam', () => {
323233
})
324234

325235
describe('missing query parameters', () => {
326-
it('returns undefined when query param missing with parser and no default', () => {
236+
it('handles missing query param with default', () => {
327237
const matcher = new MatcherPatternQueryParam(
328-
'count',
329-
'c',
238+
'userId',
239+
'user_id',
330240
'value',
331-
PARAM_PARSER_INT
241+
PARAM_PARSER_DEFAULTS,
242+
'default'
332243
)
333-
expect(matcher.match({ other: 'value' })).toEqual({ count: undefined })
244+
expect(matcher.match({})).toEqual({
245+
userId: 'default',
246+
})
247+
})
248+
249+
it('throws if a required param is missing and no default', () => {
250+
const matcher = new MatcherPatternQueryParam(
251+
'userId',
252+
'user_id',
253+
'value',
254+
PARAM_PARSER_DEFAULTS
255+
)
256+
expect(() => matcher.match({})).toThrow(MatchMiss)
334257
})
335258

336259
it('uses default when query param missing', () => {
@@ -419,14 +342,15 @@ describe('MatcherPatternQueryParam', () => {
419342
'test',
420343
'test_param',
421344
'value',
422-
{}
345+
{},
346+
'default'
423347
)
424348
// Should use PARAM_PARSER_DEFAULTS.get which returns value ?? null
425349
expect(matcher.match({ test_param: 'value' })).toEqual({
426350
test: 'value',
427351
})
428352
expect(matcher.match({ test_param: null })).toEqual({ test: null })
429-
expect(matcher.match({})).toEqual({ test: undefined })
353+
expect(matcher.match({})).toEqual({ test: 'default' })
430354
})
431355

432356
it('should handle array format with missing get method', () => {
@@ -444,22 +368,6 @@ describe('MatcherPatternQueryParam', () => {
444368
test: ['single'],
445369
})
446370
})
447-
448-
it('should handle both format with missing get method', () => {
449-
const matcher = new MatcherPatternQueryParam(
450-
'test',
451-
'test_param',
452-
'both',
453-
{}
454-
)
455-
// Should use PARAM_PARSER_DEFAULTS.get which returns value ?? null
456-
expect(matcher.match({ test_param: 'value' })).toEqual({
457-
test: 'value',
458-
})
459-
expect(matcher.match({ test_param: ['a', 'b'] })).toEqual({
460-
test: ['a', 'b'],
461-
})
462-
})
463371
})
464372

465373
describe('build', () => {
@@ -498,22 +406,6 @@ describe('MatcherPatternQueryParam', () => {
498406
test_param: ['1', 'true'],
499407
})
500408
})
501-
502-
it('should handle both format with missing set method', () => {
503-
const matcher = new MatcherPatternQueryParam(
504-
'test',
505-
'test_param',
506-
'both',
507-
{}
508-
)
509-
// Should use PARAM_PARSER_DEFAULTS.set
510-
expect(matcher.build({ test: 'value' })).toEqual({
511-
test_param: 'value',
512-
})
513-
expect(matcher.build({ test: ['a', 'b'] })).toEqual({
514-
test_param: ['a', 'b'],
515-
})
516-
})
517409
})
518410
})
519411
})

packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
MatcherQueryParams,
77
} from './matcher-pattern'
88
import { ParamParser, PARAM_PARSER_DEFAULTS } from './param-parsers'
9+
import { miss } from './errors'
910

1011
/**
1112
* Handles the `query` part of a URL. It can transform a query object into an
@@ -65,7 +66,7 @@ export class MatcherPatternQueryParam<T, ParamName extends string>
6566

6667
// if we have no values, we want to fall back to the default value
6768
if (
68-
(this.format === 'both' || this.defaultValue !== undefined) &&
69+
this.defaultValue !== undefined &&
6970
(value as unknown[]).length === 0
7071
) {
7172
value = undefined
@@ -86,9 +87,16 @@ export class MatcherPatternQueryParam<T, ParamName extends string>
8687
}
8788
}
8889

90+
// miss if there is no default and there was no value in the query
91+
if (value === undefined) {
92+
if (this.defaultValue === undefined) {
93+
throw miss()
94+
}
95+
value = toValue(this.defaultValue)
96+
}
97+
8998
return {
90-
[this.paramName]:
91-
value === undefined ? toValue(this.defaultValue) : value,
99+
[this.paramName]: value,
92100
// This is a TS limitation
93101
} as Record<ParamName, T>
94102
}

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
MatcherPatternPathDynamic,
55
} from './matcher-pattern'
66
import { MatcherPatternPathStar } from './matcher-pattern-path-star'
7+
import { miss } from './errors'
8+
import { definePathParamParser } from './param-parsers/types'
79

810
describe('MatcherPatternPathStatic', () => {
911
describe('match()', () => {
@@ -415,4 +417,48 @@ describe('MatcherPatternPathDynamic', () => {
415417
expect(pattern.build({ teamId: ['123', '456'] })).toBe('/teams/123/456/')
416418
expect(pattern.build({ teamId: [] })).toBe('/teams/')
417419
})
420+
421+
describe('custom param parsers', () => {
422+
const doubleParser = definePathParamParser({
423+
get: (v: string | null) => {
424+
const value = Number(v) * 2
425+
if (!Number.isFinite(value)) {
426+
throw miss()
427+
}
428+
return value
429+
},
430+
set: (v: number | null) => (v == null ? null : String(v / 2)),
431+
})
432+
433+
it('single regular param', () => {
434+
const pattern = new MatcherPatternPathDynamic(
435+
/^\/teams\/([^/]+?)$/i,
436+
{
437+
teamId: [doubleParser],
438+
},
439+
['teams', 1]
440+
)
441+
442+
expect(pattern.match('/teams/123')).toEqual({ teamId: 246 })
443+
expect(() => pattern.match('/teams/abc')).toThrow()
444+
expect(pattern.build({ teamId: 246 })).toBe('/teams/123')
445+
})
446+
447+
it('can transform optional params', () => {
448+
const pattern = new MatcherPatternPathDynamic(
449+
/^\/teams(?:\/([^/]+?))?$/i,
450+
{
451+
teamId: [doubleParser, false, true],
452+
},
453+
['teams', 1]
454+
)
455+
456+
expect(pattern.match('/teams')).toEqual({ teamId: 0 })
457+
expect(pattern.match('/teams/123')).toEqual({ teamId: 246 })
458+
expect(() => pattern.match('/teams/abc')).toThrow()
459+
expect(pattern.build({ teamId: 246 })).toBe('/teams/123')
460+
expect(pattern.build({ teamId: 0 })).toBe('/teams/0')
461+
expect(pattern.build({ teamId: null })).toBe('/teams')
462+
})
463+
})
418464
})

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expectTypeOf, it } from 'vitest'
22
import { MatcherPatternPathDynamic } from './matcher-pattern'
3-
import { PARAM_INTEGER_SINGLE } from './param-parsers/integers'
43
import { PATH_PARAM_PARSER_DEFAULTS } from './param-parsers'
54
import { PATH_PARAM_SINGLE_DEFAULT } from './param-parsers'
5+
import { definePathParamParser } from './param-parsers/types'
66

77
describe('MatcherPatternPathDynamic', () => {
88
it('can be generic', () => {
@@ -11,7 +11,6 @@ describe('MatcherPatternPathDynamic', () => {
1111
{ userId: [PATH_PARAM_PARSER_DEFAULTS] },
1212
['users', 1]
1313
)
14-
1514
expectTypeOf(matcher.match('/users/123')).toEqualTypeOf<{
1615
userId: string | string[] | null
1716
}>()
@@ -49,11 +48,33 @@ describe('MatcherPatternPathDynamic', () => {
4948
})
5049

5150
it('can be a custom type', () => {
51+
// naive number parser but types should be good
52+
const numberParser = definePathParamParser({
53+
get: value => {
54+
return Number(value)
55+
},
56+
set: (value: number | null) => {
57+
return String(value ?? 0)
58+
},
59+
})
60+
61+
expectTypeOf(numberParser.get('0')).toEqualTypeOf<number>()
62+
expectTypeOf(numberParser.set(0)).toEqualTypeOf<string>()
63+
expectTypeOf(numberParser.set(null)).toEqualTypeOf<string>()
64+
numberParser.get(
65+
// @ts-expect-error: must be a string
66+
null
67+
)
68+
numberParser.set(
69+
// @ts-expect-error: must be a number or null
70+
'0'
71+
)
72+
5273
const matcher = new MatcherPatternPathDynamic(
5374
/^\/profiles\/([^/]+)$/i,
5475
{
5576
userId: [
56-
PARAM_INTEGER_SINGLE,
77+
numberParser,
5778
// parser: PATH_PARAM_DEFAULT_PARSER,
5879
],
5980
},

0 commit comments

Comments
 (0)