UNPKG

zod-v4-mocks

Version:
437 lines (436 loc) 18.5 kB
import { allLocales } from '@faker-js/faker'; import { randomBytes } from 'crypto'; import { intersection as intersectionES, merge } from 'es-toolkit'; import RandExp from 'randexp'; import { z } from 'zod/v4'; function calcMinMaxString(minLength, maxLength) { if (typeof minLength === 'number') { if (maxLength === null) return { length: minLength }; return { length: { min: minLength, max: maxLength } }; } if (typeof maxLength === 'number') { if (minLength === null) return { length: maxLength }; return { length: { min: minLength, max: maxLength } }; } return { length: undefined }; } function compareMin(leftMin, rightMin) { if (typeof leftMin === 'number' && typeof rightMin === 'number') return Math.max(leftMin, rightMin); if (typeof leftMin === 'number') return leftMin; if (typeof rightMin === 'number') return rightMin; return null; } function compareMax(leftMax, rightMax) { if (typeof leftMax === 'number' && typeof rightMax === 'number') return Math.min(leftMax, rightMax); if (typeof leftMax === 'number') return leftMax; if (typeof rightMax === 'number') return rightMax; return null; } function calcMinMaxInt(minValue, maxValue) { const min = minValue === -Infinity || minValue === null ? undefined : minValue; const max = maxValue === Infinity || maxValue === null ? undefined : maxValue; if (min === undefined && max !== undefined) return { min: max - 100, max }; if (min !== undefined && max === undefined) return { min, max: min + 100 }; return { min, max }; } function calcMinMaxBigInt(minValue, maxValue) { const min = minValue === null ? undefined : minValue; const max = maxValue === null ? undefined : maxValue; if (min === undefined && max !== undefined) return { min: max - 100n, max }; if (min !== undefined && max === undefined) return { min, max: min + 100n }; return { min, max }; } function calcMinMaxFloat(minValue, maxValue) { const min = minValue === null ? undefined : minValue === Number.MIN_VALUE ? Number.MIN_SAFE_INTEGER : minValue; const max = maxValue === null ? undefined : maxValue === Number.MAX_VALUE ? Number.MAX_SAFE_INTEGER : maxValue; if (min === undefined && max !== undefined) return { min: max - 100, max }; if (min !== undefined && max === undefined) return { min, max: min + 100 }; return { min, max }; } function unwrapSchema(schema) { if (schema instanceof z.ZodOptional) return schema.unwrap(); if (schema instanceof z.ZodNullable) return schema.unwrap(); if (schema instanceof z.ZodDefault) return schema.unwrap(); if (schema instanceof z.ZodReadonly) return schema.def.innerType; if (schema instanceof z.ZodLazy) return schema.unwrap(); return schema; } /** * @package */ export const generators = { file: () => new File([], 'test.txt'), email: (faker) => faker.internet.email(), url: (faker) => faker.internet.url(), jwt: (faker) => faker.internet.jwt(), emoji: (faker) => faker.internet.emoji(), uuidv4: (faker) => faker.string.uuid(), nanoid: (faker) => faker.string.nanoid(), ulid: (faker) => faker.string.ulid(), ipv4: (faker) => faker.internet.ipv4(), ipv6: (faker) => faker.internet.ipv6(), date: (faker) => faker.date.anytime(), cidrv6: () => { return new RandExp(/([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])/).gen(); }, base64url: () => randomBytes(16).toString('base64url'), isoDateTime: (faker) => faker.date.anytime().toISOString(), isoDate: (faker) => faker.date.anytime().toISOString().split('T')[0], isoTime: (faker) => faker.date.anytime().toISOString().slice(11, 19), isoDuration: (faker) => { const hours = faker.number.int({ min: 0, max: 23 }); const minutes = faker.number.int({ min: 0, max: 59 }); const seconds = faker.number.int({ min: 0, max: 59 }); return `PT${hours}H${minutes}M${seconds}S`; }, regex: (faker, check) => { const { pattern } = check._zod.def; if (pattern !== undefined) { const randexp = new RandExp(pattern); return randexp.gen(); } return faker.lorem.word(); }, string: (faker, schema) => { const { minLength, maxLength } = schema; return faker.lorem.word(calcMinMaxString(minLength, maxLength)); }, int: (faker, schema) => { const { minValue, maxValue } = schema; const { min, max } = calcMinMaxInt(minValue, maxValue); if (min !== undefined && max !== undefined) { if (min > max) { throw new Error('Min value should be less than max value'); } } return faker.number.int({ min, max }); }, bigInt: (faker, schema) => { const { minValue, maxValue } = schema; const { min, max } = calcMinMaxBigInt(minValue, maxValue); if (min !== undefined && max !== undefined) { if (min > max) { throw new Error('Min value should be less than max value'); } } return faker.number.bigInt({ min, max }); }, float: (faker, schema) => { const { minValue, maxValue } = schema; const { min, max } = calcMinMaxFloat(minValue, maxValue); if (min !== undefined && max !== undefined) { if (min > max) { throw new Error('Min value should be less than max value'); } } return faker.number.float({ min, max }); }, literal: (schema, options) => { const { faker } = options; return faker.helpers.arrayElement([...schema.values]); }, enum: (schema, options) => { const { faker } = options; return faker.helpers.arrayElement(schema.options); }, array: (schema, options, generator) => { const { faker, config } = options; const length = faker.number.int(config.array); return Array.from({ length }, (_, arrayIndex) => { const arrayIndexes = [...options.arrayIndexes, arrayIndex]; return generator(schema.element, { ...options, arrayIndexes }); }); }, tuple: (schema, options, generator) => { const { faker, config } = options; const length = faker.number.int(config.array); return Array.from({ length }, (_, arrayIndex) => { const randomOption = faker.helpers.arrayElement(schema.def.items); const arrayIndexes = [...options.arrayIndexes, arrayIndex]; return generator(randomOption, { ...options, arrayIndexes }); }); }, map: (schema, options, generator) => { const { faker, config } = options; const { keyType, valueType } = schema; const length = faker.number.int(config.map); return new Map(Array.from({ length }, () => { const key = generator(keyType, options); const value = generator(valueType, options); return [key, value]; })); }, set: (schema, options, generator) => { const { faker, config } = options; const { valueType } = schema.def; const length = faker.number.int(config.set); return new Set(Array.from({ length }, () => generator(valueType, options))); }, object: (schema, options, generator) => { const { shape } = schema; const result = {}; for (const [key, value] of Object.entries(shape)) { result[key] = generator(value, options); } return result; }, record: (schema, options, generator) => { const { faker, config } = options; const { keyType, valueType } = schema; const length = faker.number.int(config.record); return [...Array(length)].reduce((acc) => { const key = generator(keyType, options); const value = generator(valueType, options); if (typeof key === 'string' || typeof key === 'number' || typeof key === 'symbol') return { ...acc, [key]: value }; throw new Error('Invalid record key type'); }, {}); }, union: (schema, options, generator) => { const { faker } = options; const randomOption = faker.helpers.arrayElement(schema.options); return generator(randomOption, options); }, intersection: (schema, options, generator) => { const { faker } = options; const { left, right } = schema.def; const unwrappedLeft = unwrapSchema(left); const unwrappedRight = unwrapSchema(right); if (unwrappedLeft.constructor !== left.constructor || unwrappedRight.constructor !== right.constructor) { const intersected = z.intersection(unwrappedLeft, unwrappedRight); return generator(intersected, options); } if (left instanceof z.ZodAny || left instanceof z.ZodUnknown) { return generator(right, options); } if (right instanceof z.ZodAny || right instanceof z.ZodUnknown) { return generator(left, options); } if (left instanceof z.ZodMap && right instanceof z.ZodMap) { const { keyType: leftKeyType, valueType: leftValueType } = left; const { keyType: rightKeyType, valueType: rightValueType } = right; if (leftKeyType.constructor === rightKeyType.constructor && leftValueType.constructor === rightValueType.constructor) { const newMapSchema = z.map(rightKeyType, rightValueType); return generator(newMapSchema, options); } throw new Error('Incompatible Map types in intersection'); } if (left instanceof z.ZodLiteral && right instanceof z.ZodLiteral) { const intersection = intersectionES([...left.values], [...right.values]); const mergedSchema = z.literal(intersection); return generator(mergedSchema, options); } if (left instanceof z.ZodSet && right instanceof z.ZodSet) { const { valueType: leftValueType } = left.def; const { valueType: rightValueType } = right.def; if (leftValueType.constructor === rightValueType.constructor) { const newSetSchema = z.set(rightValueType); return generator(newSetSchema, options); } throw new Error('Incompatible Set types in intersection'); } if (left instanceof z.ZodString && right instanceof z.ZodString) { const min = compareMin(left.minLength, right.minLength); const max = compareMax(left.maxLength, right.maxLength); if (min === null) { if (max === null) return generator(z.string(), options); return generator(z.string().max(max), options); } if (max === null) return generator(z.string().min(min), options); if (min > max) throw new Error('Min value should be less than max value'); return generator(z.string().min(min).max(max), options); } if (left instanceof z.ZodNumber && right instanceof z.ZodNumber) { const min = Math.max(left.minValue ?? Number.MIN_SAFE_INTEGER, right.minValue ?? Number.MIN_SAFE_INTEGER); const max = Math.min(left.maxValue ?? Number.MAX_SAFE_INTEGER, right.maxValue ?? Number.MAX_SAFE_INTEGER); return generator(z.number().min(min).max(max), options); } if (left instanceof z.ZodBoolean && right instanceof z.ZodBoolean) { return generator(right, options); } if (left instanceof z.ZodDate && right instanceof z.ZodDate) { return generator(right, options); } if (left instanceof z.ZodBigInt && right instanceof z.ZodBigInt) { const leftMin = left.minValue ?? BigInt(Number.MIN_SAFE_INTEGER); const rightMin = right.minValue ?? BigInt(Number.MIN_SAFE_INTEGER); const leftMax = left.maxValue ?? BigInt(Number.MAX_SAFE_INTEGER); const rightMax = right.maxValue ?? BigInt(Number.MAX_SAFE_INTEGER); const min = leftMin > rightMin ? leftMin : rightMin; const max = leftMax < rightMax ? leftMax : rightMax; return generator(z.bigint().min(min).max(max), options); } if (left instanceof z.ZodEnum && right instanceof z.ZodEnum) { const commonOptions = intersectionES(left.options, right.options); if (commonOptions.length === 0) { throw new Error('No common values in enum intersection'); } return faker.helpers.arrayElement(commonOptions); } if (left instanceof z.ZodUnion && right instanceof z.ZodUnion) { const { options: leftOptions } = left; const { options: rightOptions } = right; const commonTypes = []; for (const leftOption of leftOptions) { for (const rightOption of rightOptions) { if (leftOption.constructor === rightOption.constructor) { const intersected = z.intersection(leftOption, rightOption); commonTypes.push(intersected); } } } if (commonTypes.length === 0) { throw new Error('No compatible types in union intersection'); } const selectedType = faker.helpers.arrayElement(commonTypes); return generator(selectedType, options); } const leftResult = generator(left, options); const rightResult = generator(right, options); if (Array.isArray(leftResult) && Array.isArray(rightResult)) { return intersectionES(leftResult, rightResult); } if (typeof leftResult === 'object' && leftResult !== null && typeof rightResult === 'object' && rightResult !== null) { return merge(leftResult, rightResult); } const leftType = typeof leftResult; const rightType = typeof rightResult; if (leftType !== rightType) { throw new Error(`Incompatible types in intersection: ${leftType} & ${rightType}. Cannot create a value that satisfies both conditions.`); } return rightResult; }, optional: (schema, options, generator) => { const { faker, config } = options; const { optionalProbability: probability } = config; if (faker.datatype.boolean({ probability })) return undefined; return generator(schema.unwrap(), options); }, nullable: (schema, options, generator) => { const { faker, config } = options; const { nullableProbability: probability } = config; if (faker.datatype.boolean({ probability })) return null; return generator(schema.unwrap(), options); }, default: (schema, options, generator) => { const { faker, config } = options; const { defaultProbability: probability } = config; if (faker.datatype.boolean({ probability })) return schema.def.defaultValue; return generator(schema.unwrap(), options); }, lazy: (schema, options, generator) => { const { lazyDepth = 1, config } = options; const innerSchema = schema.unwrap(); if (lazyDepth < config.lazyDepthLimit) { const newOptions = { ...options, lazyDepth: lazyDepth + 1 }; return generator(innerSchema, newOptions); } if (innerSchema instanceof z.ZodObject) return {}; if (innerSchema instanceof z.ZodRecord) return {}; if (innerSchema instanceof z.ZodArray) return []; if (innerSchema instanceof z.ZodTuple) return []; if (innerSchema instanceof z.ZodMap) return new Map(); if (innerSchema instanceof z.ZodSet) return new Set(); throw new Error('Unsupported lazy schema type'); }, pipe: (schema, options, generator) => { const { in: input, out } = schema; if (input instanceof z.ZodTransform) { const inputValue = generator(out, options); const ctx = { value: inputValue, issues: [] }; return input.def.transform(inputValue, ctx); } if (out instanceof z.ZodTransform) { const inputValue = generator(input, options); const ctx = { value: inputValue, issues: [] }; return out.def.transform(inputValue, ctx); } return generator(out, options); }, transform: (schema) => schema.def.transform, catch: (schema, options, generator) => { const innerType = schema.unwrap(); const value = generator(innerType, options); const parsedValue = schema.safeParse(value); return parsedValue.data; }, }; /** * @package */ export function getLocales(locales) { const defaultLocales = [allLocales.en, allLocales.base]; if (!locales) return defaultLocales; const localesArray = Array.isArray(locales) ? locales : [locales]; return [ ...new Set([ ...localesArray.map((locale) => allLocales[locale]), ...defaultLocales, ]), ]; } /** * @package */ export function createMockConfig(config) { const { seed = 1, array: { min: arrayMin = 1, max: arrayMax = 3 } = { min: 1, max: 3 }, map: { min: mapMin = 1, max: mapMax = 3 } = { min: 1, max: 3 }, set: { min: setMin = 1, max: setMax = 3 } = { min: 1, max: 3 }, record: { min: recordMin = 1, max: recordMax = 3 } = { min: 1, max: 3 }, optionalProbability = 0.5, nullableProbability = 0.5, defaultProbability = 0.5, lazyDepthLimit = 5, ...rest } = config || {}; return { seed, array: { min: arrayMin, max: arrayMax }, map: { min: mapMin, max: mapMax }, set: { min: setMin, max: setMax }, record: { min: recordMin, max: recordMax }, optionalProbability, nullableProbability, defaultProbability, lazyDepthLimit, ...rest, }; }