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) { // Remove bracketed indices // if numbers only return the string let output = string.replace(/\[\d+\]/g, '').replace(/([A-Z])/g, ' $1') output = output.charAt(0).toUpperCase() + output.slice(1) return output } /** * Parse string and extract the index * @param string * @returns index or undefined */ export function getIndexIfArray(string: string) { const indexRegex = /\[(\d+)\]/ // Match the index const match = string.match(indexRegex) // Extract the index (number) const index = match ? Number.parseInt(match[1]) : undefined return index } /** * 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 | null { if (!schema) return null; 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) { const baseSchema = getBaseSchema(schema) return baseSchema ? baseSchema._def.typeName : ''; } /** * Search for a "ZodDefault" 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 } 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 } function isIndex(value: unknown): value is number { return Number(value) >= 0; } /** * Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax */ export function normalizeFormPath(path: string): string { const pathArr = path.split('.') if (!pathArr.length) return ''; let fullPath = String(pathArr[0]) for (let i = 1; i < pathArr.length; i++) { if (isIndex(pathArr[i])) { fullPath += `[${pathArr[i]}]` continue } fullPath += `.${pathArr[i]}` } return fullPath } type NestedRecord = Record | { [k: string]: NestedRecord } /** * Checks if the path opted out of nested fields using `[fieldName]` syntax */ export function isNotNestedPath(path: string) { return /^\[.+\]$/.test(path) } function isObject(obj: unknown): obj is Record { return obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj); } function isContainerValue(value: unknown): value is Record { return isObject(value) || Array.isArray(value) } function cleanupNonNestedPath(path: string) { if (isNotNestedPath(path)) return path.replace(/\[|\]/g, ''); return path } /** * Gets a nested property value from an object */ export function getFromPath(object: NestedRecord | undefined, path: string): TValue | undefined export function getFromPath( object: NestedRecord | undefined, path: string, fallback?: TFallback, ): TValue | TFallback export function getFromPath( object: NestedRecord | undefined, path: string, fallback?: TFallback, ): TValue | TFallback | undefined { if (!object) return fallback if (isNotNestedPath(path)) return object[cleanupNonNestedPath(path)] as TValue | undefined const resolvedValue = (path || '') .split(/\.|\[(\d+)\]/) .filter(Boolean) .reduce((acc, propKey) => { if (isContainerValue(acc) && propKey in acc) return acc[propKey] return fallback }, object as unknown) return resolvedValue as TValue | undefined } type Booleanish = boolean | 'true' | 'false' export function booleanishToBoolean(value: Booleanish) { switch (value) { case 'true': case true: return true; case 'false': case false: return false; } } export function maybeBooleanishToBoolean(value?: Booleanish) { return value ? booleanishToBoolean(value) : undefined }