diff --git a/.changeset/funny-onions-return.md b/.changeset/funny-onions-return.md new file mode 100644 index 00000000..470590cc --- /dev/null +++ b/.changeset/funny-onions-return.md @@ -0,0 +1,5 @@ +--- +"@traversable/zod": patch +--- + +feat(zod): adds `zx.toPath` support for enum records (#554) diff --git a/.changeset/ten-rabbits-care.md b/.changeset/ten-rabbits-care.md new file mode 100644 index 00000000..e55a77f2 --- /dev/null +++ b/.changeset/ten-rabbits-care.md @@ -0,0 +1,5 @@ +--- +"@traversable/zod-test": patch +--- + +docs(zod-test): fixes typo and a few broken links in the README (#550) diff --git a/packages/zod-test/README.md b/packages/zod-test/README.md index 82cba663..ac1d1e36 100644 --- a/packages/zod-test/README.md +++ b/packages/zod-test/README.md @@ -75,15 +75,15 @@ import { zxTest } from '@traversable/zod-test' ## Table of contents -- [`zxTest.fuzz`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxfuzz) +- [`zxTest.fuzz`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestfuzz) - [`zxTest.seedToSchema`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtoschema) - [`zxTest.seedToValidData`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtovaliddata) - [`zxTest.seedToInvalidData`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtoinvaliddata) -- [`zxTest.seedToValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedtovaliddatagenerator) -- [`zxTest.seedToInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedtoinvaliddatagenerator) +- [`zxTest.seedToValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtovaliddatagenerator) +- [`zxTest.seedToInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedtoinvaliddatagenerator) - [`zxTest.SeedGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedgenerator) -- [`zxTest.SeedValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedvaliddatagenerator) -- [`zxTest.SeedInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxseedinvaliddatagenerator) +- [`zxTest.SeedValidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedvaliddatagenerator) +- [`zxTest.SeedInvalidDataGenerator`](https://github.com/traversable/schema/tree/main/packages/zod-test#zxtestseedinvaliddatagenerator) ### `zxTest.fuzz` @@ -97,22 +97,23 @@ Override individual arbitraries via the 3rd argument (`overrides`). > [!NOTE] > > `zxTest.fuzz` is the __only__ schema-to-generator function that has itself -> been fuzz tested to ensure that no matter what schema you give it, the data-generator that `fuzz` -> returns will always produce valid data. - -The only known exceptions are schemas that make it impossible to generate valid data. For example: - -- `z.never` -- `z.nonoptional(z.undefined())` -- `z.enum([])` -- `z.union([])` -- `z.intersection(z.number(), z.string())` +> been fuzz tested to ensure that no matter what schema you give it, the data-generator it +> returns will always produce valid data. +> +> This excludes schemas that make it impossible to generate valid data, for example: +> +> - `z.never` +> - `z.nonoptional(z.undefined())` +> - `z.enum([])` +> - `z.union([])` +> - `z.intersection(z.number(), z.string())` #### Example ```typescript import * as vi from 'vitest' -import * as fc from 'fast-check' * import { fuzz } from '@traversable/zod-test' +import * as fc from 'fast-check' +import { fuzz } from '@traversable/zod-test' const Schema = z.record( z.string(), diff --git a/packages/zod/src/to-paths.ts b/packages/zod/src/to-paths.ts index f876c852..692fc65c 100644 --- a/packages/zod/src/to-paths.ts +++ b/packages/zod/src/to-paths.ts @@ -60,11 +60,10 @@ export declare namespace toPaths { interface Config extends Required {} } -export function walk(x: z.ZodType) { - return F.fold((x) => { +export function fold(x: z.ZodType) { + return F.fold((x, _, original) => { switch (true) { default: return fn.exhaustive(x) - // nullary case tagged('any')(x): return [[[], x._zod.def.type]] case tagged('bigint')(x): return [[[], x._zod.def.type]] case tagged('boolean')(x): return [[[], x._zod.def.type]] @@ -83,7 +82,6 @@ export function walk(x: z.ZodType) { case tagged('undefined')(x): return [[[], x._zod.def.type]] case tagged('unknown')(x): return [[[], x._zod.def.type]] case tagged('void')(x): return [[[], x._zod.def.type]] - // unary case tagged('transform')(x): return [[[], x._zod.def.type]] case tagged('array')(x): return [[[Sym.array, ...x._zod.def.element[0][0]], x._zod.def.element[0][1]]] 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) { [[Sym.mapKey, ...x._zod.def.keyType[0][0]], x._zod.def.keyType[0][1]], [[Sym.mapValue, ...x._zod.def.valueType[0][0]], x._zod.def.valueType[0][1]], ] - case tagged('record')(x): return [ - [[Sym.recordKey, ...x._zod.def.keyType[0][0]], x._zod.def.keyType[0][1]], - [[Sym.recordValue, ...x._zod.def.valueType[0][0]], x._zod.def.valueType[0][1]], - ] + case tagged('record')(x): { + if (tagged('record')(original) && tagged('enum', original._zod.def.keyType)) { + const keys = Object.values(original._zod.def.keyType._zod.def.entries) + return x._zod.def.valueType.flatMap(([path, leaf]) => keys.map((k) => [[k, ...path], leaf])) + } else { + return [ + [[Sym.recordKey, ...x._zod.def.keyType[0][0]], x._zod.def.keyType[0][1]], + [[Sym.recordValue, ...x._zod.def.valueType[0][0]], x._zod.def.valueType[0][1]], + ] + } + } case tagged('pipe')(x): return [ [[Sym.pipe, ...x._zod.def.in[0][0]], x._zod.def.in[0][1]], [[Sym.pipe, ...x._zod.def.out[0][0]], x._zod.def.out[0][1]], @@ -125,7 +130,7 @@ export function walk(x: z.ZodType) { export function toPaths(type: z.ZodType, options?: toPaths.Options): (keyof any)[][] { const $ = parseOptions(options) - return walk(type).map( + return fold(type).map( fn.flow( ([path, leaf]) => [interpreter($.interpreter, ...path), leaf] satisfies [any, any], ([path]) => path diff --git a/packages/zod/test/to-paths.test.ts b/packages/zod/test/to-paths.test.ts index e60e2071..a405aabb 100644 --- a/packages/zod/test/to-paths.test.ts +++ b/packages/zod/test/to-paths.test.ts @@ -4,7 +4,7 @@ import { z } from "zod" import { zx } from "@traversable/zod" vi.describe("〖️⛳️〗‹‹‹ ❲@traversable/zod❳: zx.paths", () => { - vi.test("〖️⛳️〗› ❲zx.toPaths❳ ", () => { + vi.test("〖️⛳️〗› ❲zx.toPaths❳: z.object ", () => { vi.expect.soft(zx.toPaths(z.object())).toMatchInlineSnapshot (`[]`) @@ -37,4 +37,60 @@ vi.describe("〖️⛳️〗‹‹‹ ❲@traversable/zod❳: zx.paths", () => { ] `) }) + + vi.test("〖️⛳️〗› ❲zx.toPaths❳: z.record", () => { + vi.expect.soft(zx.toPaths( + z.record(z.enum([]), z.object({ + a: z.number(), + b: z.object({ + c: z.string(), + d: z.boolean(), + }), + })) + )).toMatchInlineSnapshot + (`[]`) + + // https://github.com/traversable/schema/issues/554 + vi.expect.soft(zx.toPaths( + z.record(z.enum(['left', 'right']), z.object({ + a: z.number(), + b: z.object({ + c: z.string(), + d: z.boolean(), + }), + })) + )).toMatchInlineSnapshot + (` + [ + [ + "left", + "a", + ], + [ + "right", + "a", + ], + [ + "left", + "b", + "c", + ], + [ + "right", + "b", + "c", + ], + [ + "left", + "b", + "d", + ], + [ + "right", + "b", + "d", + ], + ] + `) + }) })