import { z } from 'zod' import * as fc from 'fast-check' import type { inline } from '@traversable/registry' import { Array_isArray, fn, isKeyOf, isObject, isShowable, mutateRandomElementOf, mutateRandomValueOf, Number_isFinite, Number_isNatural, Number_isSafeInteger, Object_assign, Object_entries, Object_fromEntries, Object_keys, Object_values, omit, pair, PATTERN, pick, symbol, } from '@traversable/registry' type Config = import('./generator-options.js').Config import * as Config from './generator-options.js' import * as Bounds from './generator-bounds.js' import type { Tag } from './generator-seed.js' import { byTag, bySeed, Seed, fold } from './generator-seed.js' import { PromiseSchemaIsUnsupported, removePrototypeMethods, } from './utils.js' const identifier = fc.stringMatching(new RegExp(PATTERN.identifier, 'u')) function getDefaultValue(x: z.ZodType) { return x._zod.def.type === 'undefined' || x._zod.def.type === 'void' ? undefined : {} } const enumValues = fc.uniqueArray( fc.tuple( identifier, /** * Can't use numeric values when generating `z.enum` without a workaround for this issue: * https://github.com/colinhacks/zod/issues/4353 */ identifier, // fc.oneof(fc.string(), fc.integer()), ), { selector: ([k]) => k, minLength: 1, } ).map(Object_fromEntries) satisfies fc.Arbitrary const literalValue = fc.oneof( fc.string({ minLength: Bounds.defaults.string[0], maxLength: Bounds.defaults.string[1] }), fc.double({ min: Bounds.defaults.number[0], max: Bounds.defaults.number[1], noNaN: true }), fc.bigInt({ min: Bounds.defaults.bigint[0], max: Bounds.defaults.bigint[1] }), fc.boolean(), ) const templateLiteralValue = fc.oneof( fc.string({ minLength: 1, maxLength: Bounds.defaults.string[1] }), fc.double({ min: Bounds.defaults.number[0], max: Bounds.defaults.number[1], noNaN: true }), fc.bigInt({ min: Bounds.defaults.bigint[0], max: Bounds.defaults.bigint[1] }), fc.boolean(), ) const TerminalMap = { any: fn.const(fc.tuple(fc.constant(byTag.any))), boolean: fn.const(fc.tuple(fc.constant(byTag.boolean))), date: fn.const(fc.tuple(fc.constant(byTag.date))), file: fn.const(fc.tuple(fc.constant(byTag.file))), nan: fn.const(fc.tuple(fc.constant(byTag.nan))), never: fn.const(fc.tuple(fc.constant(byTag.never))), null: fn.const(fc.tuple(fc.constant(byTag.null))), undefined: fn.const(fc.tuple(fc.constant(byTag.undefined))), unknown: fn.const(fc.tuple(fc.constant(byTag.unknown))), void: fn.const(fc.tuple(fc.constant(byTag.void))), symbol: fn.const(fc.tuple(fc.constant(byTag.symbol))), } satisfies { [K in keyof Seed.TerminalMap]: SeedBuilder } const bigIntBounds = Bounds.bigint(fc.bigInt()) const integerBounds = Bounds.int(fc.integer()) const numberBounds = Bounds.number(fc.double()) const stringBounds = Bounds.string(fc.integer({ min: 0 })) const BoundableMap = { bigint: fn.const(fc.tuple(fc.constant(byTag.bigint), bigIntBounds)), int: fn.const(fc.tuple(fc.constant(byTag.int), integerBounds)), number: fn.const(fc.tuple(fc.constant(byTag.number), numberBounds)), string: fn.const(fc.tuple(fc.constant(byTag.string), stringBounds)), } satisfies { [K in keyof Seed.BoundableMap]: SeedBuilder } const ValueMap = { enum: fn.const(fc.tuple(fc.constant(byTag.enum), enumValues)), literal: fn.const(fc.tuple(fc.constant(byTag.literal), literalValue)), template_literal: (_tie, $) => templateLiteralSeed($), } satisfies { [K in keyof Seed.ValueMap]: SeedBuilder } const UnaryMap = { array: (tie) => fc.tuple(fc.constant(byTag.array), tie('*'), Bounds.array(fc.integer({ min: 0 }))), catch: (tie) => fc.tuple(fc.constant(byTag.catch), tie('*')), custom: (tie) => fc.tuple(fc.constant(byTag.custom), tie('*')), default: (tie) => fc.tuple(fc.constant(byTag.default), tie('*')), prefault: (tie) => fc.tuple(fc.constant(byTag.prefault), tie('*')), lazy: (tie) => fc.tuple(fc.constant(byTag.lazy), fc.func<[], unknown>(tie('*'))), nonoptional: (tie) => fc.tuple(fc.constant(byTag.nonoptional), tie('*')), nullable: (tie) => fc.tuple(fc.constant(byTag.nullable), tie('*')), optional: (tie) => fc.tuple(fc.constant(byTag.optional), tie('*')), readonly: (tie) => fc.tuple(fc.constant(byTag.readonly), tie('*')), record: (tie) => fc.tuple(fc.constant(byTag.record), tie('*')), set: (tie) => fc.tuple(fc.constant(byTag.set), tie('*')), success: (tie) => fc.tuple(fc.constant(byTag.success), tie('*')), object: (tie, $) => fc.tuple( fc.constant(byTag.object), fc.uniqueArray(fc.tuple(identifier.filter(removePrototypeMethods), tie('*')), $) ), tuple: (tie, $) => fc.tuple(fc.constant(byTag.tuple), fc.array(tie('*'), $)), union: (tie, $) => fc.tuple(fc.constant(byTag.union), fc.array(tie('*'), $)), intersection: (tie) => entries(tie('*'), { minLength: 2 }).map(fn.flow( (xs) => pair( xs.slice(0, Math.ceil(xs.length / 2)), xs.slice(Math.ceil(xs.length / 2)), ), ([l, r]) => pair( pair(byTag.object, l), pair(byTag.object, r), ), (both) => pair(byTag.intersection, both), )), map: (tie) => fc.tuple(fc.constant(byTag.map), fc.tuple(tie('*'), tie('*'))), pipe: (tie) => fc.tuple(fc.constant(byTag.pipe), fc.tuple(tie('*'), tie('*'))), transform: (tie) => fc.tuple(fc.constant(byTag.transform), tie('*')), promise: (tie) => fc.tuple(fc.constant(byTag.promise), tie('*')), } satisfies { [K in keyof Seed.UnaryMap]: SeedBuilder } const TerminalSeeds = fn.map(Object_keys(TerminalMap), (tag) => byTag[tag]) const BoundableSeeds = fn.map(Object_keys(BoundableMap), (tag) => byTag[tag]) export interface SeedBuilder { (tie: fc.LetrecTypedTie, $: Config.byTypeName[K]): fc.Arbitrary } export type SeedMap = { [K in keyof Seed]: SeedBuilder } export const SeedMap = { ...TerminalMap, ...BoundableMap, ...ValueMap, ...UnaryMap, } satisfies SeedMap export function isTerminal(x: unknown): x is Seed.Terminal | Seed.Boundable export function isTerminal(x: unknown) { if (!Array_isArray(x)) return false else { const tag = x[0] as never return TerminalSeeds.includes(tag) || BoundableSeeds.includes(tag) } } export const pickAndSortNodes : (nodes: readonly ([keyof SeedMap, unknown])[]) => ($: Config) => (keyof SeedMap)[] = (nodes) => ({ include, exclude, sortBias } = Config.defaults as never) => nodes .map(([k]) => k) .filter((x) => (include ? include.includes(x as never) : true) && (exclude ? !exclude.includes(x as never) : true) ) // TODO: remove nullish coalesce operators .sort((l, r) => sortBias[l]! < sortBias[r]! ? -1 : sortBias[l]! > sortBias[r]! ? 1 : 0) export const z_int : (bounds?: Bounds.int) => z.ZodNumber = (bounds = Bounds.defaults.int) => { const [min, max, multipleOf] = bounds let schema = z.number().int() if (Number_isSafeInteger(min)) schema = schema.min(min) if (Number_isSafeInteger(max)) schema = schema.max(max) // if (Number_isSafeInteger(multipleOf) && !Number_isNaN(multipleOf)) schema = schema.multipleOf(multipleOf) return schema } export const z_bigint : (bounds?: Bounds.bigint) => z.ZodBigInt = (bounds = Bounds.defaults.bigint) => { const [min, max, multipleOf] = bounds let schema = z.bigint() if (typeof min === 'bigint') schema = schema.min(min) if (typeof max === 'bigint') schema = schema.max(max) // if (typeof multipleOf === 'bigint') schema = schema.multipleOf(multipleOf) return schema } export const z_number : (bounds?: Bounds.number) => z.ZodNumber = (bounds = Bounds.defaults.number) => { const [min, max, multipleOf, minExcluded, maxExcluded] = bounds let schema = z.number() if (Number_isFinite(min)) schema = minExcluded ? schema.gt(min) : schema.min(min) if (Number_isFinite(max)) schema = maxExcluded ? schema.lt(max) : schema.max(max) // if (typeof multipleOf === 'bigint') schema = schema.multipleOf(multipleOf) return schema } export const z_string : (bounds?: Bounds.string) => z.ZodString = (bounds = Bounds.defaults.string) => { const [min, max, exactLength] = bounds let schema = z.string() if (Number_isNatural(exactLength)) return ( schema = schema.min(exactLength), schema = schema.max(exactLength), schema ) else { if (Number_isNatural(min)) schema = schema.min(min) if (Number_isNatural(max)) schema = schema.max(max) return schema } } export const z_array : (elementSchema: T, bounds?: Bounds.array) => z.ZodArray = (elementSchema, bounds = Bounds.defaults.array) => { const [min, max, exactLength] = bounds let schema = z.array(elementSchema) if (Number_isNatural(exactLength)) return ( schema = schema.min(exactLength), schema = schema.max(exactLength), schema ) else { if (Number_isNatural(min)) schema = schema.min(min) if (Number_isNatural(max)) schema = schema.max(max) return schema } } const unboundedSeed = { int: () => fc.constant([byTag.int, [null, null, null]]), bigint: () => fc.constant([byTag.bigint, [null, null, null]]), number: () => fc.constant([byTag.number, [null, null, null, false, false]]), string: () => fc.constant([byTag.string, [null, null]]), array: (tie) => fc.tuple(fc.constant(byTag.array), tie('*'), fc.constant([null, null])), } satisfies Record fc.Arbitrary> export interface Builder extends inline<{ [K in Tag]+?: fc.Arbitrary }> { root?: fc.Arbitrary invalid?: fc.Arbitrary ['*']: fc.Arbitrary } export function Builder(base: Gen.Base): >( options?: Options, overrides?: Partial> ) => (tie: fc.LetrecLooselyTypedTie) => Builder export function Builder(base: Gen.Base) { return >(options?: Options, overrides?: Partial>) => { const $ = Config.parseOptions(options) return (tie: fc.LetrecLooselyTypedTie) => { const builder: { [x: string]: fc.Arbitrary } = fn.pipe( { ...base, ...$.int.unbounded && 'int' in base && { int: unboundedSeed.int }, ...$.bigint.unbounded && 'bigint' in base && { bigint: unboundedSeed.bigint }, ...$.number.unbounded && 'number' in base && { number: unboundedSeed.number }, ...$.string.unbounded && 'string' in base && { string: unboundedSeed.string }, ...$.array.unbounded && 'array' in base && { array: unboundedSeed.array }, ...overrides, }, (x) => pick(x, $.include), (x) => omit(x, $.exclude), (x) => fn.map(x, (f, k) => f(tie, $[k as never])), ) const nodes = pickAndSortNodes(Object_entries(builder) as [k: keyof SeedMap, unknown][])($) builder['*'] = fc.oneof($['*'], ...nodes.map((k) => builder[k])) const root = isKeyOf(builder, $.root) && builder[$.root] let leaf = builder['*'] return Object_assign( builder, { ...root && { root }, ['*']: leaf }) } } } export declare namespace Gen { type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary } type Values = never | T[Exclude] type InferArb = S extends fc.Arbitrary ? T : never /* @ts-expect-error */ interface Builder extends T { ['*']: fc.Arbitrary>> } type BuildBuilder, Out extends {} = BuilderBase> = never | Builder type BuilderBase, $ extends ParseOptions = ParseOptions> = never | & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> }) & { [K in Exclude<$['include'], $['exclude']>]: fc.Arbitrary } type ParseOptions> = never | { include: Options['include'] extends readonly unknown[] ? Options['include'][number] : keyof T exclude: Options['exclude'] extends readonly unknown[] ? Options['exclude'][number] : never root: Options['root'] extends keyof T ? T[Options['root']] : never } } /** * ## {@link Gen `Gen`} */ export function Gen(base: Gen.Base): >( options?: Options, overrides?: Partial> ) => Gen.BuildBuilder export function Gen(base: Gen.Base) { return >( options?: Options, overrides?: Partial> ): Builder => { return fc.letrec(Builder(base)(options, overrides)) } } const typedArray = fc.oneof( fc.int8Array(), fc.uint8Array(), fc.uint8ClampedArray(), fc.int16Array(), fc.uint16Array(), fc.int32Array(), fc.uint32Array(), fc.float32Array(), fc.float64Array(), fc.bigInt64Array(), fc.bigUint64Array(), ) const pathName = fc.webUrl().map((webUrl) => new URL(webUrl).pathname) const ext = fc.string({ minLength: 2, maxLength: 3 }) const fileName = fc.tuple(pathName, ext).map(([pathName, ext]) => `${pathName}.${ext}` as const) const fileBits = fc.array(typedArray) const file = fc.tuple(fileBits, fileName).map(([fileBits, filename]) => new File(fileBits, filename)) const arbitrarySymbol = fc.oneof(fc.constant(Symbol()), fc.string().map((s) => Symbol.for(s))) function entries(model: fc.Arbitrary, options?: fc.UniqueArrayConstraintsRecommended<[k: string, T], unknown>) { return fc.uniqueArray( fc.tuple(identifier.filter(removePrototypeMethods), model), { selector: options?.selector || (([k]) => k), ...options }, ) } const is = { null: (x: unknown): x is [byTag['null']] => Array_isArray(x) && x[0] === byTag.null, undefined: (x: unknown): x is [byTag['undefined']] => Array_isArray(x) && x[0] === byTag.undefined, boolean: (x: unknown): x is [byTag['boolean']] => Array_isArray(x) && x[0] === byTag.boolean, int: (x: unknown): x is [byTag['int'], Bounds.int] => Array_isArray(x) && x[0] === byTag.int, number: (x: unknown): x is [byTag['number'], Bounds.number] => Array_isArray(x) && x[0] === byTag.number, string: (x: unknown): x is [byTag['number'], Bounds.string] => Array_isArray(x) && x[0] === byTag.string, literal: (x: unknown): x is [byTag['literal'], z.core.util.Literal] => Array_isArray(x) && x[0] === byTag.literal, bigint: (x: unknown): x is [byTag['number'], Bounds.bigint] => Array_isArray(x) && x[0] === byTag.bigint, nullable: (x: unknown): x is [byTag['nullable'], TemplateLiteralTerminal] => Array_isArray(x) && x[0] === byTag.nullable, optional: (x: unknown): x is [byTag['optional'], TemplateLiteralTerminal] => Array_isArray(x) && x[0] === byTag.optional, enum: (x: unknown): x is [byTag['enum'], { [x: string]: number | string }] => Array_isArray(x) && x[0] === byTag.enum, union: (x: unknown): x is [byTag['optional'], readonly TemplateLiteralTerminal[]] => Array_isArray(x) && x[0] === byTag.union, } function templateLiteralNodeToPart(x: Seed.TemplateLiteral.Node): z.core.$ZodTemplateLiteralPart { if (isShowable(x)) return z.literal(x) else if (is.null(x)) return z.null() else if (is.undefined(x)) return z.undefined() else if (is.boolean(x)) return z.boolean() else if (is.int(x)) return z_int(x[1]) else if (is.number(x)) return z_number(x[1]) else if (is.bigint(x)) return z_bigint(x[1]) else if (is.string(x)) return z_string(x[1]) else if (is.literal(x)) return z.literal(x[1]) else if (is.literal(x)) return z.literal(x[1]) else if (is.enum(x)) return z.enum(x[1]) else if (is.nullable(x)) { return z.nullable(templateLiteralNodeToPart(x[1]) as z.ZodType) as z.core.$ZodTemplateLiteralPart } else if (is.optional(x)) { return z.optional(templateLiteralNodeToPart(x[1]) as z.ZodType) as z.core.$ZodTemplateLiteralPart } else { return fn.exhaustive(x as never) } } function templateParts(seed: Seed.TemplateLiteral) { return fn.map(seed[1], templateLiteralNodeToPart) } function generateStringFromRegExp(regex: RegExp, $: Config) { return regex.source === '^()$' ? fc.constant('') : fc.stringMatching(regex, $.template_literal) } function templateLiteralSeed($: Config.byTypeName['template_literal']): fc.Arbitrary { return fc.tuple( fc.constant(byTag.template_literal), fc.array(templateLiteralPart($), $), ) } type TemplateLiteralTerminal = | null | undefined | string | number | bigint | boolean | [40] | [50] | [15] | [200, Bounds.number] | [150, Bounds.bigint] | [250, Bounds.string] | [550, string | number | bigint | boolean] const templateLiteralTerminals = fc.oneof( fc.constant(null), fc.constant(undefined), fc.constant(''), fc.boolean(), fc.integer(), fc.bigInt(), fc.string(), TerminalMap.undefined(), TerminalMap.null(), TerminalMap.boolean(), BoundableMap.bigint(), BoundableMap.number(), BoundableMap.string(), ValueMap.literal(), ) satisfies fc.Arbitrary function templateLiteralPart($: Config.byTypeName['template_literal']) { return fc.oneof( $, { arbitrary: fc.constant(null), weight: 1 }, { arbitrary: fc.constant(undefined), weight: 2 }, // { arbitrary: fc.constant(''), weight: 3 }, { arbitrary: fc.boolean(), weight: 4 }, { arbitrary: fc.integer(), weight: 5 }, { arbitrary: fc.bigInt(), weight: 6 }, { arbitrary: fc.string({ minLength: 1 }), weight: 7 }, { arbitrary: TerminalMap.undefined(), weight: 8 }, { arbitrary: TerminalMap.null(), weight: 9 }, { arbitrary: TerminalMap.boolean(), weight: 10 }, { arbitrary: BoundableMap.bigint(), weight: 11 }, { arbitrary: BoundableMap.number(), weight: 12 }, { arbitrary: BoundableMap.string(), weight: 13 }, { arbitrary: templateLiteralValue, weight: 14 }, // { arbitrary: fc.tuple(fc.constant(byTag.nullable), templateLiteralTerminals), weight: 15 }, // { arbitrary: fc.tuple(fc.constant(byTag.optional), templateLiteralTerminals), weight: 16 }, ) satisfies fc.Arbitrary } function intersect(x: unknown, y: unknown) { return !isObject(x) ? y : !isObject(y) ? x : Object_assign(x, y) } const GeneratorByTag = { any: () => fc.anything(), boolean: () => fc.boolean(), date: () => fc.date({ noInvalidDate: true }), file: () => file, nan: () => fc.constant(Number.NaN), never: () => fc.constant(void 0 as never), null: () => fc.constant(null), symbol: () => arbitrarySymbol, undefined: () => fc.constant(undefined), unknown: () => fc.anything(), void: () => fc.constant(void 0 as void), int: (x) => fc.integer(Bounds.intBoundsToIntegerConstraints(x[1])), bigint: (x) => fc.bigInt(Bounds.bigintBoundsToBigIntConstraints(x[1])), number: (x) => fc.double(Bounds.numberBoundsToDoubleConstraints(x[1])), string: (x) => fc.string(Bounds.stringBoundsToStringConstraints(x[1])), enum: (x) => fc.constantFrom(...Object_values(x[1])), literal: (x) => fc.constant(x[1]), template_literal: (x, $) => generateStringFromRegExp(z.templateLiteral(templateParts(x))._zod.pattern, $), array: (x) => fc.array(x[1], Bounds.arrayBoundsToArrayConstraints(x[2])), nonoptional: (x) => x[1].map((_) => _ === undefined ? {} : _), nullable: (x) => fc.option(x[1], { nil: null }), optional: (x, _$, isProperty) => isProperty ? x[1] : fc.option(x[1], { nil: undefined }), readonly: (x) => x[1], set: (x) => x[1].map((v) => new globalThis.Set([v])), success: (x) => x[1], catch: (x) => x[1], map: (x) => fc.tuple(x[1][0], x[1][1]).map(([k, v]) => new Map([[k, v]])), record: (x) => fc.dictionary(fc.string().filter(removePrototypeMethods), x[1]), tuple: (x) => fc.tuple(...x[1]), union: (x) => fc.oneof(...(x[1] || [fc.constant(void 0 as never)])), lazy: (x) => x[1](), default: (x) => x[1], prefault: (x) => x[1], custom: (x) => x[1], pipe: (x) => x[1][1], object: (x) => fc.record(Object.fromEntries(x[1])), transform: (x) => x[1], intersection: (x) => fc.tuple(...x[1]).map(([x, y]) => intersect(x, y)), promise: () => PromiseSchemaIsUnsupported('GeneratorByTag'), } satisfies { [K in keyof Seed]: (x: Seed>[K], $: Config, isProperty: boolean) => fc.Arbitrary } /** * ## {@link seedToValidDataGenerator `seedToValidDataGenerator`} * * Convert a seed into an valid data generator. * * Valid in this context means that it will always satisfy the zod schema that the seed produces. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}. * * To convert a seed to an _invalid_ data generator, use {@link seedToInvalidDataGenerator `seedToInvalidDataGenerator`}. */ export function seedToValidDataGenerator(seed: Seed.F, options?: Config.Options): fc.Arbitrary export function seedToValidDataGenerator(seed: Seed.F, options?: Config.Options): fc.Arbitrary { const $ = Config.parseOptions(options) return fold>((x, isProperty) => GeneratorByTag[bySeed[x[0]]](x as never, $, isProperty || x[0] === 7500))(seed as never) } /** * ## {@link seedToInvalidDataGenerator `seedToInvalidDataGenerator`} * * Convert a seed into an invalid data generator. * * Invalid in this context means that it will never satisfy the zod schema that the seed produces. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}. * * To convert a seed to an _valid_ data generator, use * {@link seedToValidDataGenerator `seedToValidDataGenerator`}. */ export function seedToInvalidDataGenerator(seed: T, options?: Config.Options): fc.Arbitrary export function seedToInvalidDataGenerator(seed: Seed.F, options?: Config.Options): fc.Arbitrary export function seedToInvalidDataGenerator(seed: Seed.F, options?: Config.Options): fc.Arbitrary { const $ = Config.parseOptions(options) return fold>((x) => { switch (x[0]) { case byTag.record: return GeneratorByTag.record(x).map(mutateRandomValueOf) case byTag.array: return GeneratorByTag.array(x).map(mutateRandomElementOf) case byTag.tuple: return GeneratorByTag.tuple(x).map(mutateRandomElementOf) case byTag.object: return GeneratorByTag.object(x).map(mutateRandomValueOf) default: return GeneratorByTag[bySeed[x[0]]](x as never, $, false) } })(seed as never) } /** * ## {@link SeedGenerator `SeedGenerator`} * * Pseudo-random seed generator. * * The generator supports a wide range of discoverable configuration options via the * optional `options` argument. * * Many of those options are forwarded to the corresponding `fast-check` arbitrary. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * See also: * - {@link SeedGenerator `SeedGenerator`} * * @example * import * as fc from 'fast-check' * import { z } from 'zod' * import { zxTest } from '@traversable/zod-test' * * const Json = zxTest.SeedGenerator({ include: ['null', 'boolean', 'number', 'string', 'array', 'object'] }) * const [jsonNumber, jsonObject, anyJson] = [ * fc.sample(Json.number, 1)[0], * fc.sample(Json.object, 1)[0], * fc.sample(Json['*'], 1)[0], * ] as const * * console.log(JSON.stringify(jsonNumber)) * // => [200,[2.96e-322,1,null,false,true]] * * console.log(zxTest.toString(zxTest.seedToSchema(jsonNumber))) * // => z.number().min(2.96e-322).lt(1) * * console.log(JSON.stringify(jsonObject)) * // => [7500,[["n;}289K~",[250,[null,null]]]]] * * console.log(zxTest.toString(zxTest.seedToSchema(jsonObject))) * // => z.object({ "n;}289K~": z.string() }) * * console.log(anyJson) * // => [250,[23,64]] * * console.log(zxTest.toString(zxTest.seedToSchema(anyJson))) * // => z.string().min(23).max(64) */ export const SeedGenerator = Gen(SeedMap) const seedsThatPreventGeneratingValidData = [ 'never', 'nonoptional', 'pipe', 'promise', ] satisfies SchemaGenerator.Options['exclude'] const seedsThatPreventGeneratingInvalidData = [ 'any', 'catch', 'custom', 'default', 'prefault', 'never', 'nonoptional', 'pipe', 'promise', 'symbol', 'transform', 'unknown', 'success', 'prefault', ] satisfies SchemaGenerator.Options['exclude'] /** * ## {@link SeedValidDataGenerator `SeedValidDataGenerator`} * * A seed generator that can be interpreted to produce reliably valid data. * * This was originally developed to test for parity between various schema libraries. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * Note that certain schemas make generating valid data impossible * (like {@link z.never `z.never`}) or or prohibitively difficult * (like {@link z.pipe `z.pipe`}). For this reason, those schemas are not seeded. * * To see the list of excluded schemas, see * {@link seedsThatPreventGeneratingValidData `seedsThatPreventGeneratingValidData`}. * * See also: * - {@link SeedInvalidDataGenerator `SeedInvalidDataGenerator`} * * @example * import * as fc from 'fast-check' * import { z } from 'zod' * import { zxTest } from '@traversable/zod-test' * * const [seed] = fc.sample(zxTest.SeedValidDataGenerator, 1) * const ZodSchema = zxTest.seedToSchema(seed) * const dataset = fc.sample(zxTest.seedToValidData(seed), 5) * * const results = dataset.map((data) => ZodSchema.safeParse(data).success) * * console.log(results) // => [true, true, true, true, true] */ export const SeedValidDataGenerator = SeedGenerator({ exclude: seedsThatPreventGeneratingValidData })['*'] /** * ## {@link SeedInvalidDataGenerator `zxTest.SeedInvalidDataGenerator`} * * A seed generator that can be interpreted to produce reliably invalid data. * * This was originally developed to test for parity between various schema libraries. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * Note that certain schemas make generating invalid data impossible * (like {@link z.any `z.any`}) or prohibitively difficult * (like {@link z.catch `z.catch`}). For this reason, those schemas are not seeded. * * To see the list of excluded schemas, see * {@link seedsThatPreventGeneratingInvalidData `zxTest.seedsThatPreventGeneratingInvalidData`}. * * See also: * - {@link SeedValidDataGenerator `zxTest.SeedValidDataGenerator`} * * @example * import * as fc from 'fast-check' * import { z } from 'zod' * import { zxTest } from '@traversable/zod-test' * * const [seed] = fc.sample(zxTest.SeedInvalidDataGenerator, 1) * const ZodSchema = zxTest.seedToSchema(seed) * const dataset = fc.sample(zxTest.seedToInvalidData(seed), 5) * * const results = dataset.map((data) => ZodSchema.safeParse(data).success) * * console.log(results) // => [false, false, false, false, false] */ export const SeedInvalidDataGenerator = fn.pipe( SeedGenerator({ exclude: seedsThatPreventGeneratingInvalidData }), ($) => fc.oneof( $.object, $.tuple, $.array, $.record, ) ) /** * ## {@link SchemaGenerator `zxTest.SchemaGenerator`} * * A zod schema generator that can be interpreted to produce an arbitrary `zod` schema (v4, classic). * * The generator supports a wide range of configuration options that are discoverable via the * optional `options` argument. * * Many of those options are forwarded to the corresponding `fast-check` arbitrary. * * To use it, you'll need to have [`fast-check`](https://github.com/dubzzz/fast-check) installed. * * See also: * - {@link SeedGenerator `zxTest.SeedGenerator`} * * @example * import * as fc from 'fast-check' * import { z } from 'zod' * import { zxTest } from '@traversable/zod-test' * * const tenSchemas = fc.sample(zxTest.SchemaGenerator({ * include: ['null', 'boolean', 'number', 'string', 'array', 'object'] * }), 10) * * tenSchemas.forEach((s) => console.log(zxTest.toString(s))) * // => z.number() * // => z.string().max(64) * // => z.null() * // => z.array(z.boolean()) * // => z.boolean() * // => z.object({ "": z.object({ "/d2P} {/": z.boolean() }), "svH2]L'x": z.number().lt(-65536) }) * // => z.null() * // => z.string() * // => z.array(z.array(z.null())) * // => z.object({ "y(Qza": z.boolean(), "G1S\\U 4Y6i": z.object({ "YtO3]ia0cM": z.boolean() }) }) */ export const SchemaGenerator = fn.flow( SeedGenerator, builder => builder['*'], (arb) => arb.map(seedToSchema), ) export declare namespace SchemaGenerator { type Options = Config.Options } /** * ## {@link seedToSchema `zxTest.seedToSchema`} * * Interpreter that converts a seed value into the corresponding zod schema. * * To get a seed, use {@link SeedGenerator `zxTest.SeedGenerator`}. */ export function seedToSchema(seed: T): Seed.schemaFromComposite[T[0]] export function seedToSchema(seed: Seed.F): z.ZodType export function seedToSchema(seed: Seed.F) { return fold((x) => { switch (true) { default: return fn.exhaustive(x) case x[0] === byTag.any: return z.any() case x[0] === byTag.boolean: return z.boolean() case x[0] === byTag.date: return z.date() case x[0] === byTag.file: return z.file() case x[0] === byTag.nan: return z.nan() case x[0] === byTag.never: return z.never() case x[0] === byTag.null: return z.null() case x[0] === byTag.symbol: return z.symbol() case x[0] === byTag.undefined: return z.undefined() case x[0] === byTag.unknown: return z.unknown() case x[0] === byTag.void: return z.void() case x[0] === byTag.int: return z_int(x[1]) case x[0] === byTag.bigint: return z_bigint(x[1]) case x[0] === byTag.number: return z_number(x[1]) case x[0] === byTag.string: return z_string(x[1]) case x[0] === byTag.enum: return z.enum(x[1]) case x[0] === byTag.literal: return z.literal(x[1]) case x[0] === byTag.template_literal: return z.templateLiteral(templateParts(x)) case x[0] === byTag.array: return z.array(x[1]) case x[0] === byTag.nonoptional: return z.nonoptional(x[1]) case x[0] === byTag.nullable: return z.nullable(x[1]) case x[0] === byTag.optional: return z.optional(x[1]) case x[0] === byTag.readonly: return z.readonly(x[1]) case x[0] === byTag.set: return z.set(x[1]) case x[0] === byTag.success: return z.success(x[1]) case x[0] === byTag.catch: return z.catch(x[1], {}) case x[0] === byTag.default: return z._default(x[1], getDefaultValue(x[1])) case x[0] === byTag.prefault: return z.prefault(x[1], getDefaultValue(x[1])) case x[0] === byTag.intersection: return z.intersection(...x[1]) case x[0] === byTag.map: return z.map(x[1][0], x[1][1]) case x[0] === byTag.record: return z.record(z.string(), x[1]) case x[0] === byTag.object: return z.object(Object.fromEntries(x[1])) case x[0] === byTag.tuple: return z.tuple(x[1] as []) case x[0] === byTag.union: return z.union(x[1]) case x[0] === byTag.pipe: return z.pipe(...x[1]) case x[0] === byTag.custom: return z.custom() case x[0] === byTag.transform: return z.transform(() => x[1]) case x[0] === byTag.lazy: return z.lazy(x[1]) case x[0] === byTag.promise: return PromiseSchemaIsUnsupported('seedToSchema') } })(seed as never) } /** * ## {@link seedToValidData `seedToValidData`} * * Given a seed, generates an single example of valid data. * * Valid in this context means that it will always satisfy the zod schema that the seed produces. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}. * * To convert a seed to a single example of _invalid_ data, use {@link seedToInvalidData `seedToInvalidData`}. */ export const seedToValidData = fn.flow( seedToValidDataGenerator, (model) => fc.sample(model, 1)[0], ) /** * ## {@link seedToInvalidData `seedToInvalidData`} * * Given a seed, generates an single example of invalid data. * * Invalid in this context means that it will never satisfy the zod schema that the seed produces. * * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed. * * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}. * * To convert a seed to a single example of _valid_ data, use {@link seedToValidData `seedToValidData`}. */ export const seedToInvalidData = fn.flow( seedToInvalidDataGenerator, (model) => fc.sample(model, 1)[0], )