UNPKG

rune-form

Version:

Type-safe reactive form builder for Svelte 5

213 lines (212 loc) 7.81 kB
import { z, ZodArray, ZodDefault, ZodNullable, ZodNumber, ZodObject, ZodOptional, ZodUnion } from 'zod'; // --- Caching for performance --- const shapeCache = new WeakMap(); const elementCache = new WeakMap(); const unwrappedCache = new WeakMap(); const pathsCache = new WeakMap(); function getShape(schema) { if (!shapeCache.has(schema)) { let shape = {}; if (schema._def && typeof schema._def.shape === 'function') { shape = schema._def.shape(); } else if (schema._def && schema._def.shape) { shape = schema._def.shape; } shapeCache.set(schema, shape); } return shapeCache.get(schema); } function getElement(schema) { if (!elementCache.has(schema)) { let element = z.unknown(); if (schema._def && schema._def.element) { element = schema._def.element; } elementCache.set(schema, element); } return elementCache.get(schema); } function unwrapSchema(schema) { if (unwrappedCache.has(schema)) return unwrappedCache.get(schema); let s = schema; while (s.isOptional?.() || s.isNullable?.() || s instanceof ZodDefault || s instanceof ZodOptional || s instanceof ZodNullable) { s = s.def.innerType; } unwrappedCache.set(schema, s); return s; } export function getZodInputConstraints(schema) { const constraints = {}; // -- Helper: unwrap inner schema const unwrap = (s) => { while (s instanceof ZodOptional || s instanceof ZodDefault || s instanceof ZodNullable) { s = s.def.innerType; } return s; }; // -- Helper: check if schema is optional-ish const isOptionalish = (s) => { return s instanceof ZodOptional || s instanceof ZodDefault || s instanceof ZodNullable; }; // required flag based on original schema constraints.required = !isOptionalish(schema); const base = unwrap(schema); switch (true) { case base instanceof ZodNumber: { constraints.type = 'number'; for (const check of base.def.checks ?? []) { if (check && typeof check === 'object' && 'kind' in check) { switch (check.kind) { case 'min': if ('value' in check) constraints.min = check.value; break; case 'max': if ('value' in check) constraints.max = check.value; break; case 'int': constraints.step = 1; break; } } } break; } case base instanceof ZodArray: { constraints.type = 'array'; break; } // Optional: Add more cases for boolean, date, enum, etc. } return constraints; } function flattenZodIssues(issues) { const errors = {}; for (const issue of issues) { const path = issue.path.join('.'); if (!errors[path]) errors[path] = []; errors[path].push(issue.message); } return errors; } export function getAllPaths(schema, base = '', depth = 0, maxDepth = 8) { schema = unwrapSchema(schema); if (!schema || typeof schema !== 'object') return []; if (depth > maxDepth) return []; // Handle ZodUnion if (schema instanceof ZodUnion) { const options = schema._def.options; const all = options.flatMap((opt) => getAllPaths(opt, base, depth + 1, maxDepth)); return [...new Set(all)]; } if (schema instanceof ZodObject) { const shape = getShape(schema); const childPaths = Object.entries(shape).flatMap(([key, sub]) => getAllPaths(sub, base ? `${base}.${key}` : key, depth + 1, maxDepth)); return base ? [base, ...childPaths] : childPaths; } if (schema instanceof ZodArray) { const arrayBase = base ? `${base}.0` : '0'; const element = getElement(schema); const inner = getAllPaths(element, arrayBase, depth + 1, maxDepth); return base ? [base, ...inner] : inner; } return base ? [base] : []; } // --- Precompute all valid paths for a schema (once) --- export function getPrecomputedPaths(schema, maxDepth = 8) { if (pathsCache.has(schema)) return pathsCache.get(schema) || []; const paths = getAllPaths(schema, '', 0, maxDepth); pathsCache.set(schema, paths); return paths; } // --- Batch validation helper --- export function batchValidate(schema, data) { return schema.safeParse(data); } // --- Metadata helper (from .describe()) --- export function getFieldDescription(schema) { return schema._def?.description; } export function createZodValidator(schema) { return { parse(data) { return schema.parse(data); }, safeParse(data) { const result = schema.safeParse(data); if (result.success) return { success: true, data: result.data }; return { success: false, errors: flattenZodIssues(result.error.issues) }; }, async safeParseAsync(data) { const result = await schema.safeParseAsync(data); if (result.success) return { success: true, data: result.data }; return { success: false, errors: flattenZodIssues(result.error.issues) }; }, resolveDefaults(data) { const walk = (schema, value) => { if (schema instanceof ZodDefault) { const innerType = schema.def.innerType; const defaultValue = schema.def .defaultValue; if (value !== undefined) { return walk(innerType, value); } // Handle function defaults if (typeof defaultValue === 'function') { const defaultResult = schema.def.defaultValue(); return walk(innerType, defaultResult); } // Handle static defaults return walk(innerType, defaultValue); } if (schema instanceof ZodObject) { const result = {}; for (const key in schema.shape) { const fieldSchema = schema.shape[key]; const val = value?.[key]; result[key] = walk(fieldSchema, val); } return result; } if (schema instanceof ZodArray) { if (Array.isArray(value)) { return value.map((v) => walk(schema.def.type, v)); } return []; } // primitive or unhandled type return value !== undefined ? value : undefined; }; return walk(schema, data ?? {}); }, getPaths: () => getAllPaths(schema), getInputAttributes(path) { let current = schema; for (const key of path.split('.')) { if (current instanceof ZodObject) { current = current.shape[key]; } else if (current instanceof ZodArray && /^\d+$/.test(key)) { current = current.def.type; } else { return {}; } } return getZodInputConstraints(current); // 🧠 Zod-specific utility } }; }