UNPKG

@volverjs/form-vue

Version:

Vue 3 Forms with @volverjs/ui-vue

457 lines (415 loc) 17.6 kB
import { ZodError, } from 'zod/v3' import { $ZodError, safeParse as z4SafeParse, safeParseAsync as z4SafeParseAsync, formatError as z4FormatError, } from 'zod/v4/core' import type * as z3 from 'zod/v3' import type * as z4 from 'zod/v4/core' import type { FormSchema, InferSchema, VvZodError, ZodIssue } from './types' // Helper function to determine the type of a value function _getValueType(value: unknown) { if (Array.isArray(value)) { return 'array' } if (value === null) { return 'null' } return typeof value } // Helper function to check if a value matches a schema type function _isValueCompatibleWithSchema(value: unknown, subSchema: z4.JSONSchema.JSONSchema): boolean { const valueType = _getValueType(value) if (subSchema.type) { return subSchema.type === valueType || (subSchema.type === 'integer' && valueType === 'number' && Number.isInteger(value as number)) } // If no type specified, assume compatibility return true } export const isZod3Object = (value: z3.ZodTypeAny): value is z3.ZodObject<any> => { return value._def.typeName === 'ZodObject' } export const isZod4Object = (value: z4.$ZodType): value is z4.$ZodObject<any> => { return value._zod.def.type === 'object' } export const isZod3Default = (value: z3.ZodTypeAny): value is z3.ZodDefault<any> => { return value._def.typeName === 'ZodDefault' } export const isZod4Default = (value: z4.$ZodType): value is z4.$ZodDefault<any> => { return value._zod.def.type === 'default' } export const isZod3Nullable = (value: z3.ZodTypeAny): value is z3.ZodNullable<any> => { return value._def.typeName === 'ZodNullable' } export const isZod4Nullable = (value: z4.$ZodType): value is z4.$ZodNullable<any> => { return value._zod.def.type === 'nullable' } export const isZod3Record = (value: z3.ZodTypeAny): value is z3.ZodRecord<any, any> => { return value._def.typeName === 'ZodRecord' } export const isZod4Record = (value: z4.$ZodType): value is z4.$ZodRecord<any, any> => { return value._zod.def.type === 'record' } export const isZod3Array = (value: z3.ZodTypeAny): value is z3.ZodArray<any> => { return value._def.typeName === 'ZodArray' } export const isZod4Array = (value: z4.$ZodType): value is z4.$ZodArray<any> => { return value._zod.def.type === 'array' } export const isZod3Effects = (value: z3.ZodTypeAny): value is z3.ZodEffects<any> => { return value._def.typeName === 'ZodEffects' } export const isZod3Optional = (value: z3.ZodTypeAny): value is z3.ZodOptional<any> => { return value._def.typeName === 'ZodOptional' } export const isZod4Optional = (value: z4.$ZodType): value is z4.$ZodOptional<any> => { return value._zod.def.type === 'optional' } // zod 4 replacements for ZodEffects export const isZod4Pipe = (value: z4.$ZodType): value is z4.$ZodPipe<any> => { return value._zod.def.type === 'pipe' } export const isZod4Transform = (value: z4.$ZodType): value is z4.$ZodTransform<any> => { return value._zod.def.type === 'transform' } function _loopOnZod3Effects<Type extends z3.ZodTypeAny>( schema: Type | z3.ZodEffects<Type> | z3.ZodEffects<z3.ZodEffects<Type>>, ) { let toReturn = schema while (isZod3Effects(toReturn)) { toReturn = toReturn.innerType() } return toReturn } function _loopOnZod4Pipe<Type extends z4.$ZodType>( schema: | Type | z4.$ZodPipe<Type> | z4.$ZodPipe<any, Type>, ) { let toReturn = schema while (isZod4Pipe(toReturn)) { if (isZod4Transform(toReturn._zod.def.out)) { toReturn = toReturn._zod.def.in } else { toReturn = toReturn._zod.def.out as Type } } return toReturn } // Helper function to get the inner type of a Zod schema export const getZod3SchemaInnerType = <Type extends z3.ZodTypeAny>( schema: | Type | z3.ZodEffects<Type> | z3.ZodEffects<z3.ZodEffects<Type>> | z3.ZodOptional<Type>, ) => { let toReturn = _loopOnZod3Effects(schema) if (isZod3Optional(toReturn)) { toReturn = toReturn._def.innerType } return toReturn } export const getZod4SchemaInnerType = <Type extends z4.$ZodType>( schema: | Type | z4.$ZodPipe<Type> | z4.$ZodPipe<any, Type> | z4.$ZodOptional<Type>, ) => { let toReturn = _loopOnZod4Pipe(schema) if (isZod4Optional(toReturn)) { toReturn = toReturn._zod.def.innerType } return toReturn } // Helper function to check if a Zod schema is optional export const isZod3SchemaOptional = <Type extends z3.ZodTypeAny>( schema: | Type | z3.ZodEffects<Type> | z3.ZodEffects<z3.ZodEffects<Type>> | z3.ZodOptional<Type>, ) => { const toReturn = _loopOnZod3Effects(schema) if (isZod3Optional(toReturn)) { return true } return false } export const isZod4SchemaOptional = <Type extends z4.$ZodType>( schema: | Type | z4.$ZodPipe<Type> | z4.$ZodPipe<any, Type> | z4.$ZodOptional<Type>, ) => { const toReturn = _loopOnZod4Pipe(schema) if (isZod4Optional(toReturn)) { return true } return false } export function defaultObjectByJSONSchema(schema: z4.JSONSchema.JSONSchema, original?: unknown): unknown { // Handle anyOf - find the best matching schema without unnecessary recursion if (schema.anyOf && Array.isArray(schema.anyOf)) { if (original !== undefined) { // First pass: find exact type match for (const subSchema of schema.anyOf) { if (_isValueCompatibleWithSchema(original, subSchema as z4.JSONSchema.JSONSchema)) { return defaultObjectByJSONSchema(subSchema as z4.JSONSchema.JSONSchema, original) } } // Second pass: try first schema that doesn't explicitly conflict for (const subSchema of schema.anyOf) { const subSchemaTyped = subSchema as z4.JSONSchema.JSONSchema if (!subSchemaTyped.type || subSchemaTyped.type === 'object') { return defaultObjectByJSONSchema(subSchemaTyped, original) } } } // Fallback to first schema return defaultObjectByJSONSchema(schema.anyOf[0] as z4.JSONSchema.JSONSchema, original) } // Early return for non-object types if (schema.type !== 'object' || !schema.properties) { switch (schema.type) { case 'string': return typeof original === 'string' ? original : schema.default case 'number': case 'integer': return typeof original === 'number' ? original : schema.default case 'boolean': return typeof original === 'boolean' ? original : schema.default case 'null': return original === null ? original : schema.default case 'array': if (Array.isArray(original) && schema.items) { return original.map(item => defaultObjectByJSONSchema(schema.items as z4.JSONSchema.JSONSchema, item)) } return schema.default default: return schema.default } } // Object handling with optimizations const properties = schema.properties as Record<string, z4.JSONSchema.JSONSchema> const isOriginalObject = original && typeof original === 'object' && !Array.isArray(original) const originalObj = isOriginalObject ? original as Record<string, unknown> : undefined // Initialize result object more efficiently const toReturn: Record<string, unknown> = {} // Process schema properties first for (const key in properties) { const originalValue = originalObj?.[key] toReturn[key] = defaultObjectByJSONSchema(properties[key], originalValue) } // Handle additional properties after schema properties if (originalObj && schema.additionalProperties !== false) { const schemaKeys = new Set(Object.keys(properties)) for (const [key, value] of Object.entries(originalObj)) { if (!schemaKeys.has(key)) { // Allow any additional properties as-is toReturn[key] = value } } } return toReturn } export const isZod4Schema = (schema: z3.ZodTypeAny | z4.$ZodType): schema is z4.$ZodType => { if ('_zod' in schema) { return true } return false } export function defaultObjectBySchema<Schema extends FormSchema>(schema: Schema, original: Partial<InferSchema<Schema>> & Record<string, unknown> = {}): Partial<InferSchema<Schema>> { // zod v4 if (isZod4Schema(schema)) { const innerType = getZod4SchemaInnerType(schema) if (!isZod4Object(innerType)) { return original } const unknownKeys = innerType._zod.def.catchall && innerType._zod.def.catchall._zod.def.type !== 'never' return { ...(unknownKeys ? original : {}), ...Object.fromEntries( ('shape' in innerType._zod.def ? Object.entries(innerType._zod.def.shape) as [string, z4.$ZodType][] : []).map( ([key, subSchema]) => { const originalValue = original[key] const isOptional = isZod4SchemaOptional(subSchema) let innerType = getZod4SchemaInnerType(subSchema) let defaultValue: Partial<InferSchema<Schema>> | undefined if (isZod4Default(innerType)) { defaultValue = innerType._zod.def.defaultValue innerType = innerType._zod.def.innerType } if ( originalValue === null && isZod4Nullable(innerType) ) { return [key, originalValue] } if ((originalValue === undefined || originalValue === null) && isOptional) { return [key, defaultValue] } if (innerType) { const parse = z4SafeParse(subSchema, originalValue) if (parse.success) { return [key, parse.data ?? defaultValue] } } if ( isZod4Array(innerType) && Array.isArray(originalValue) && originalValue.length ) { const arrayType = getZod4SchemaInnerType(innerType._zod.def.element) if (isZod4Object(arrayType)) { return [ key, originalValue.map((element: unknown) => defaultObjectBySchema( arrayType, (element && typeof element === 'object' ? element : undefined) as Partial< typeof arrayType >, ), ), ] } } if (isZod4Record(innerType) && originalValue) { const valueType = getZod4SchemaInnerType(innerType._zod.def.valueType) if (isZod4Object(valueType)) { return [key, Object.keys(originalValue).reduce((acc: Record<string, unknown>, recordKey: string) => { acc[recordKey] = defaultObjectBySchema(valueType, originalValue[recordKey]) return acc }, {})] } } if (isZod4Object(innerType)) { return [ key, defaultObjectBySchema( innerType, originalValue && typeof originalValue === 'object' ? originalValue : defaultValue, ), ] } return [key, defaultValue] }, ), ), } as Partial<InferSchema<Schema>> } // zod v3 const innerType = getZod3SchemaInnerType(schema) if (!isZod3Object(innerType)) { return original } const unknownKeys = innerType._def.unknownKeys === 'passthrough' return { ...(unknownKeys ? original : {}), ...Object.fromEntries( ('shape' in innerType ? Object.entries(innerType.shape) as [string, z3.ZodTypeAny][] : []).map( ([key, subSchema]) => { const originalValue = original[key] const isOptional = isZod3SchemaOptional(subSchema) let innerType = getZod3SchemaInnerType(subSchema) let defaultValue: Partial<InferSchema<Schema>> | undefined if (isZod3Default(innerType)) { defaultValue = innerType._def.defaultValue() innerType = innerType._def.innerType } if ( originalValue === null && isZod3Nullable(innerType) ) { return [key, originalValue] } if ((originalValue === undefined || originalValue === null) && isOptional) { return [key, defaultValue] } if (innerType) { const parse = subSchema.safeParse(originalValue) if (parse.success) { return [key, parse.data ?? defaultValue] } } if ( isZod3Array(innerType) && Array.isArray(originalValue) && originalValue.length ) { const arrayType = getZod3SchemaInnerType(innerType._def.type) if (isZod3Object(arrayType)) { return [ key, originalValue.map((element: unknown) => defaultObjectBySchema( arrayType, (element && typeof element === 'object' ? element : undefined) as Partial< typeof arrayType >, ), ), ] } } if (isZod3Record(innerType) && originalValue) { const valueType = getZod3SchemaInnerType(innerType._def.valueType) if (isZod3Object(valueType)) { return [key, Object.keys(originalValue).reduce((acc: Record<string, unknown>, recordKey: string) => { acc[recordKey] = defaultObjectBySchema(valueType, originalValue[recordKey]) return acc }, {})] } } if (isZod3Object(innerType)) { return [ key, defaultObjectBySchema( innerType, originalValue && typeof originalValue === 'object' ? originalValue : defaultValue, ), ] } return [key, defaultValue] }, ), ), } as Partial<InferSchema<Schema>> } export const safeParseAsync = <T extends FormSchema>(schema: T, data: any) => { if (isZod4Schema(schema)) { return z4SafeParseAsync(schema, data) } return schema.safeParseAsync(data) } export const formatError = <T extends FormSchema>(schema: T, error: VvZodError<T>) => { if (isZod4Schema(schema)) { return z4FormatError(error as z4.$ZodError<T>) } return (error as z3.ZodError<T>).format() } export const formatIssues = (schema: FormSchema, issues: ZodIssue[]) => { if (isZod4Schema(schema)) { return z4FormatError(new $ZodError(issues as z4.$ZodIssue[])) } return new ZodError(issues as z3.ZodIssue[]).format() }