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; }