Skip to content

Commit deac0b9

Browse files
authored
Merge pull request #555 from traversable/zod-toPath-support-enum-records
feat(zod): adds `zx.toPath` support for enum records (#554)
2 parents a11fc43 + cc1dadf commit deac0b9

File tree

5 files changed

+98
-26
lines changed

5 files changed

+98
-26
lines changed

.changeset/funny-onions-return.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@traversable/zod": patch
3+
---
4+
5+
feat(zod): adds `zx.toPath` support for enum records (#554)

.changeset/ten-rabbits-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@traversable/zod-test": patch
3+
---
4+
5+
docs(zod-test): fixes typo and a few broken links in the README (#550)

packages/zod-test/README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ import { zxTest } from '@traversable/zod-test'
7575

7676
## Table of contents
7777

78-
- [`zxTest.fuzz`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxfuzz)
78+
- [`zxTest.fuzz`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestfuzz)
7979
- [`zxTest.seedToSchema`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtoschema)
8080
- [`zxTest.seedToValidData`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtovaliddata)
8181
- [`zxTest.seedToInvalidData`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtoinvaliddata)
82-
- [`zxTest.seedToValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedtovaliddatagenerator)
83-
- [`zxTest.seedToInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedtoinvaliddatagenerator)
82+
- [`zxTest.seedToValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtovaliddatagenerator)
83+
- [`zxTest.seedToInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtoinvaliddatagenerator)
8484
- [`zxTest.SeedGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedgenerator)
85-
- [`zxTest.SeedValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedvaliddatagenerator)
86-
- [`zxTest.SeedInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedinvaliddatagenerator)
85+
- [`zxTest.SeedValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedvaliddatagenerator)
86+
- [`zxTest.SeedInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedinvaliddatagenerator)
8787

8888

8989
### `zxTest.fuzz`
@@ -97,22 +97,23 @@ Override individual arbitraries via the 3rd argument (`overrides`).
9797
> [!NOTE]
9898
>
9999
> `zxTest.fuzz` is the __only__ schema-to-generator function that has itself
100-
> been fuzz tested to ensure that no matter what schema you give it, the data-generator that `fuzz`
101-
> returns will always produce valid data.
102-
103-
The only known exceptions are schemas that make it impossible to generate valid data. For example:
104-
105-
- `z.never`
106-
- `z.nonoptional(z.undefined())`
107-
- `z.enum([])`
108-
- `z.union([])`
109-
- `z.intersection(z.number(), z.string())`
100+
> been fuzz tested to ensure that no matter what schema you give it, the data-generator it
101+
> returns will always produce valid data.
102+
>
103+
> This excludes schemas that make it impossible to generate valid data, for example:
104+
>
105+
> - `z.never`
106+
> - `z.nonoptional(z.undefined())`
107+
> - `z.enum([])`
108+
> - `z.union([])`
109+
> - `z.intersection(z.number(), z.string())`
110110
111111
#### Example
112112

113113
```typescript
114114
import * as vi from 'vitest'
115-
import * as fc from 'fast-check' * import { fuzz } from '@traversable/zod-test'
115+
import * as fc from 'fast-check'
116+
import { fuzz } from '@traversable/zod-test'
116117

117118
const Schema = z.record(
118119
z.string(),

packages/zod/src/to-paths.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,10 @@ export declare namespace toPaths {
6060
interface Config extends Required<Options> {}
6161
}
6262

63-
export function walk(x: z.ZodType) {
64-
return F.fold<Path[]>((x) => {
63+
export function fold(x: z.ZodType) {
64+
return F.fold<Path[]>((x, _, original) => {
6565
switch (true) {
6666
default: return fn.exhaustive(x)
67-
// nullary
6867
case tagged('any')(x): return [[[], x._zod.def.type]]
6968
case tagged('bigint')(x): return [[[], x._zod.def.type]]
7069
case tagged('boolean')(x): return [[[], x._zod.def.type]]
@@ -83,7 +82,6 @@ export function walk(x: z.ZodType) {
8382
case tagged('undefined')(x): return [[[], x._zod.def.type]]
8483
case tagged('unknown')(x): return [[[], x._zod.def.type]]
8584
case tagged('void')(x): return [[[], x._zod.def.type]]
86-
// unary
8785
case tagged('transform')(x): return [[[], x._zod.def.type]]
8886
case tagged('array')(x): return [[[Sym.array, ...x._zod.def.element[0][0]], x._zod.def.element[0][1]]]
8987
case tagged('catch')(x): return [[[Sym.catch, ...x._zod.def.innerType[0][0]], x._zod.def.innerType[0][1]]]
@@ -105,10 +103,17 @@ export function walk(x: z.ZodType) {
105103
[[Sym.mapKey, ...x._zod.def.keyType[0][0]], x._zod.def.keyType[0][1]],
106104
[[Sym.mapValue, ...x._zod.def.valueType[0][0]], x._zod.def.valueType[0][1]],
107105
]
108-
case tagged('record')(x): return [
109-
[[Sym.recordKey, ...x._zod.def.keyType[0][0]], x._zod.def.keyType[0][1]],
110-
[[Sym.recordValue, ...x._zod.def.valueType[0][0]], x._zod.def.valueType[0][1]],
111-
]
106+
case tagged('record')(x): {
107+
if (tagged('record')(original) && tagged('enum', original._zod.def.keyType)) {
108+
const keys = Object.values(original._zod.def.keyType._zod.def.entries)
109+
return x._zod.def.valueType.flatMap(([path, leaf]) => keys.map<Path>((k) => [[k, ...path], leaf]))
110+
} else {
111+
return [
112+
[[Sym.recordKey, ...x._zod.def.keyType[0][0]], x._zod.def.keyType[0][1]],
113+
[[Sym.recordValue, ...x._zod.def.valueType[0][0]], x._zod.def.valueType[0][1]],
114+
]
115+
}
116+
}
112117
case tagged('pipe')(x): return [
113118
[[Sym.pipe, ...x._zod.def.in[0][0]], x._zod.def.in[0][1]],
114119
[[Sym.pipe, ...x._zod.def.out[0][0]], x._zod.def.out[0][1]],
@@ -125,7 +130,7 @@ export function walk(x: z.ZodType) {
125130

126131
export function toPaths(type: z.ZodType, options?: toPaths.Options): (keyof any)[][] {
127132
const $ = parseOptions(options)
128-
return walk(type).map(
133+
return fold(type).map(
129134
fn.flow(
130135
([path, leaf]) => [interpreter($.interpreter, ...path), leaf] satisfies [any, any],
131136
([path]) => path

packages/zod/test/to-paths.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { z } from "zod"
44
import { zx } from "@traversable/zod"
55

66
vi.describe("〖️⛳️〗‹‹‹ ❲@traversable/zod❳: zx.paths", () => {
7-
vi.test("〖️⛳️〗› ❲zx.toPaths❳ ", () => {
7+
vi.test("〖️⛳️〗› ❲zx.toPaths❳: z.object ", () => {
88
vi.expect.soft(zx.toPaths(z.object())).toMatchInlineSnapshot
99
(`[]`)
1010

@@ -37,4 +37,60 @@ vi.describe("〖️⛳️〗‹‹‹ ❲@traversable/zod❳: zx.paths", () => {
3737
]
3838
`)
3939
})
40+
41+
vi.test("〖️⛳️〗› ❲zx.toPaths❳: z.record", () => {
42+
vi.expect.soft(zx.toPaths(
43+
z.record(z.enum([]), z.object({
44+
a: z.number(),
45+
b: z.object({
46+
c: z.string(),
47+
d: z.boolean(),
48+
}),
49+
}))
50+
)).toMatchInlineSnapshot
51+
(`[]`)
52+
53+
// https://github.com/traversable/schema/issues/554
54+
vi.expect.soft(zx.toPaths(
55+
z.record(z.enum(['left', 'right']), z.object({
56+
a: z.number(),
57+
b: z.object({
58+
c: z.string(),
59+
d: z.boolean(),
60+
}),
61+
}))
62+
)).toMatchInlineSnapshot
63+
(`
64+
[
65+
[
66+
"left",
67+
"a",
68+
],
69+
[
70+
"right",
71+
"a",
72+
],
73+
[
74+
"left",
75+
"b",
76+
"c",
77+
],
78+
[
79+
"right",
80+
"b",
81+
"c",
82+
],
83+
[
84+
"left",
85+
"b",
86+
"d",
87+
],
88+
[
89+
"right",
90+
"b",
91+
"d",
92+
],
93+
]
94+
`)
95+
})
4096
})

0 commit comments

Comments
 (0)