Skip to content

Commit 2295f48

Browse files
committed
Use zodSchemaToParamSchema in parseUrlSearchParams
1 parent d990492 commit 2295f48

File tree

3 files changed

+49
-35
lines changed

3 files changed

+49
-35
lines changed

src/lib/parse.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import type { ZodTypeAny } from 'zod'
22

33
import {
4-
isZodArray,
5-
isZodBoolean,
6-
isZodNumber,
7-
isZodObject,
8-
isZodSchema,
9-
isZodString,
10-
} from './zod.js'
4+
type ParamSchema,
5+
type ValueType,
6+
zodSchemaToParamSchema,
7+
} from './schema.js'
8+
import { isZodObject } from './zod.js'
119

1210
export const parseUrlSearchParams = (
1311
query: URLSearchParams | string,
@@ -22,33 +20,48 @@ export const parseUrlSearchParams = (
2220
)
2321
}
2422

25-
// const paramSchema = zodSchemaToParamSchema(schema)
26-
// traverse paramSchema, and build a new object
27-
// for each node, lookup expected key in searchParams
28-
// if match, try to parse and include in object, otherwise, skip node
29-
30-
// TODO: For array parsing, try to lookup foo=, then foo[]= patterns,
31-
// if only one match, try to detect commas, otherwise ignore commas.
32-
// if both foo= and foo[]= this is a parse error
33-
34-
const obj: Record<string, unknown> = {}
35-
for (const [k, v] of Object.entries(
36-
schema.shape as unknown as Record<string, unknown>,
37-
)) {
38-
if (searchParams.has(k)) {
39-
if (isZodSchema(v)) obj[k] = parse(k, searchParams.getAll(k), v)
40-
}
23+
const paramSchema = zodSchemaToParamSchema(schema)
24+
return parseFromParamSchema(searchParams, paramSchema, []) as Record<
25+
string,
26+
unknown
27+
>
28+
}
29+
30+
const parseFromParamSchema = (
31+
searchParams: URLSearchParams,
32+
node: ParamSchema | ValueType,
33+
path: string[],
34+
): Record<string, unknown> | unknown => {
35+
if (typeof node === 'string') {
36+
// TODO: For array parsing, try to lookup foo=, then foo[]= patterns,
37+
// if only one match, try to detect commas, otherwise ignore commas.
38+
// if both foo= and foo[]= this is a parse error
39+
// more generally, try to find a matching key for this node in the searchParams
40+
// and throw if conflicting keys are found, e.g, both foo= and foo[]=
41+
const key = path.join('.')
42+
return parse(key, searchParams.getAll(key), node)
4143
}
4244

43-
return obj
45+
const entries = Object.entries(node).reduce<
46+
Array<[string, Record<string, unknown> | unknown]>
47+
>((acc, entry) => {
48+
const [k, v] = entry
49+
const currentPath = [...path, k]
50+
return [...acc, [k, parseFromParamSchema(searchParams, v, currentPath)]]
51+
}, [])
52+
53+
return Object.fromEntries(entries)
4454
}
4555

46-
const parse = (k: string, values: string[], schema: ZodTypeAny): unknown => {
56+
const parse = (k: string, values: string[], type: ValueType): unknown => {
4757
// TODO: Add better errors with coercion. If coercion fails, passthough?
48-
if (isZodNumber(schema)) return Number(values[0])
49-
if (isZodBoolean(schema)) return values[0] === 'true'
50-
if (isZodString(schema)) return String(values[0])
51-
if (isZodArray(schema)) return values
58+
// TODO: Is this Number parsing safe?
59+
if (values.length === 0) return undefined
60+
if (type === 'number') return Number(values[0])
61+
if (type === 'boolean') return values[0] === 'true'
62+
if (type === 'string') return String(values[0])
63+
if (type === 'string_array') return values
64+
if (type === 'number_array') return values.map((v) => Number(v))
5265
throw new UnparseableSearchParamError(k, 'unsupported type')
5366
}
5467

src/lib/schema.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ import {
1515
isZodString,
1616
} from './zod.js'
1717

18-
type ValueType =
18+
export type ValueType =
1919
| 'string'
2020
| 'number'
2121
| 'boolean'
2222
| 'date'
2323
| 'string_array'
2424
| 'number_array'
2525

26-
interface ParamSchema {
26+
export interface ParamSchema {
2727
[key: string]: ParamSchema | ValueType
2828
}
2929

@@ -47,6 +47,7 @@ const nestedZodSchemaToParamSchema = (
4747
): ParamSchema | ValueType => {
4848
if (isZodObject(schema)) {
4949
const shape = schema.shape as unknown as Record<string, unknown>
50+
5051
const entries = Object.entries(shape).reduce<
5152
Array<[string, ParamSchema | ValueType]>
5253
>((acc, entry) => {
@@ -57,6 +58,7 @@ const nestedZodSchemaToParamSchema = (
5758
}
5859
throw new UnparseableSchemaError(currentPath, 'unexpected non-zod schema')
5960
}, [])
61+
6062
return Object.fromEntries(entries)
6163
}
6264

test/parse.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { parseUrlSearchParams } from '@seamapi/url-search-params-parser'
77
test('parses empty params', (t) => {
88
const schema = z.object({ foo: z.string() })
99
const input = {}
10-
t.deepEqual(
11-
parseUrlSearchParams(serializeUrlSearchParams(input), schema),
12-
input,
13-
)
10+
t.deepEqual(parseUrlSearchParams(serializeUrlSearchParams(input), schema), {
11+
foo: undefined,
12+
})
1413
})

0 commit comments

Comments
 (0)