rune-form
Version:
Type-safe reactive form builder for Svelte 5
213 lines (212 loc) • 7.81 kB
JavaScript
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
}
};
}