import type { DefaultValues } from "react-hook-form"; import type { z } from "zod"; // TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions. export type ZodObjectOrWrapped = | z.ZodObject | z.ZodEffects>; /** * Beautify a camelCase string. * e.g. "myString" -> "My String" */ export function beautifyObjectName(string: string) { let output = string.replace(/([A-Z])/g, " $1"); output = output.charAt(0).toUpperCase() + output.slice(1); return output; } /** * Get the lowest level Zod type. * This will unpack optionals, refinements, etc. */ export function getBaseSchema< ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny >(schema: ChildType | z.ZodEffects): ChildType { if ("innerType" in schema._def) { return getBaseSchema(schema._def.innerType as ChildType); } if ("schema" in schema._def) { return getBaseSchema(schema._def.schema as ChildType); } return schema as ChildType; } /** * Get the type name of the lowest level Zod type. * This will unpack optionals, refinements, etc. */ export function getBaseType(schema: z.ZodAny): string { return getBaseSchema(schema)._def.typeName; } /** * Search for a "ZodDefult" in the Zod stack and return its value. */ export function getDefaultValueInZodStack(schema: z.ZodAny): any { const typedSchema = schema as unknown as z.ZodDefault< z.ZodNumber | z.ZodString >; if (typedSchema._def.typeName === "ZodDefault") { return typedSchema._def.defaultValue(); } if ("innerType" in typedSchema._def) { return getDefaultValueInZodStack( typedSchema._def.innerType as unknown as z.ZodAny ); } if ("schema" in typedSchema._def) { return getDefaultValueInZodStack( (typedSchema._def as any).schema as z.ZodAny ); } return undefined; } /** * Get all default values from a Zod schema. */ export function getDefaultValues>( schema: Schema ) { const { shape } = schema; type DefaultValuesType = DefaultValues>>; const defaultValues = {} as DefaultValuesType; for (const key of Object.keys(shape)) { const item = shape[key] as z.ZodAny; if (getBaseType(item) === "ZodObject") { const defaultItems = getDefaultValues( getBaseSchema(item) as unknown as z.ZodObject ); for (const defaultItemKey of Object.keys(defaultItems)) { const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType; defaultValues[pathKey] = defaultItems[defaultItemKey]; } } else { const defaultValue = getDefaultValueInZodStack(item); if (defaultValue !== undefined) { defaultValues[key as keyof DefaultValuesType] = defaultValue; } } } return defaultValues; } export function getObjectFormSchema( schema: ZodObjectOrWrapped ): z.ZodObject { if (schema._def.typeName === "ZodEffects") { const typedSchema = schema as z.ZodEffects>; return getObjectFormSchema(typedSchema._def.schema); } return schema as z.ZodObject; } /** * Convert a Zod schema to HTML input props to give direct feedback to the user. * Once submitted, the schema will be validated completely. */ export function zodToHtmlInputProps( schema: | z.ZodNumber | z.ZodString | z.ZodOptional | any ): React.InputHTMLAttributes { if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) { const typedSchema = schema as z.ZodOptional; return { ...zodToHtmlInputProps(typedSchema._def.innerType), required: false, }; } const typedSchema = schema as z.ZodNumber | z.ZodString; if (!("checks" in typedSchema._def)) return { required: true, }; const { checks } = typedSchema._def; const inputProps: React.InputHTMLAttributes = { required: true, }; const type = getBaseType(schema); for (const check of checks) { if (check.kind === "min") { if (type === "ZodString") { inputProps.minLength = check.value; } else { inputProps.min = check.value; } } if (check.kind === "max") { if (type === "ZodString") { inputProps.maxLength = check.value; } else { inputProps.max = check.value; } } } return inputProps; }