import { getTypeDefault } from './formBuilderRegistry' import type { FieldRules } from './useForm' import type { FieldCondition, FieldValidation, FormEnhancements, FormFieldSchema, FormSchema, FormSectionSchema, } from '../types/form-builder' /** * Evaluate a JSON-serializable field condition against the current form data. * * Supports logical operators (`and`, `or`, `not`) and comparison operators * (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`, * `contains`). Returns `true` if the condition passes. */ export function evaluateCondition( condition: FieldCondition, data: Record, ): boolean { if ('and' in condition) { return condition.and.every((c) => evaluateCondition(c, data)) } if ('or' in condition) { return condition.or.some((c) => evaluateCondition(c, data)) } if ('not' in condition) { return !evaluateCondition(condition.not, data) } const value = data[condition.field] if ('eq' in condition) return value === condition.eq if ('neq' in condition) return value !== condition.neq if ('gt' in condition) return typeof value === 'number' && value > condition.gt if ('lt' in condition) return typeof value === 'number' && value < condition.lt if ('gte' in condition) return typeof value === 'number' && value >= condition.gte if ('lte' in condition) return typeof value === 'number' && value <= condition.lte if ('in' in condition) return condition.in.includes(value) if ('notIn' in condition) return !condition.notIn.includes(value) if ('truthy' in condition) return !!value if ('falsy' in condition) return !value if ('contains' in condition) { if (typeof value === 'string') return value.includes(condition.contains) if (Array.isArray(value)) return value.includes(condition.contains) return false } return true } /** Return all sections across steps (wizard) or directly from a flat schema. */ export function collectSections(schema: FormSchema): FormSectionSchema[] { return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections } /** Return all field schemas in schema order, flattening sections and steps. */ export function flattenFields(schema: FormSchema): FormFieldSchema[] { return collectSections(schema).flatMap((section) => section.fields) } /** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */ function convertValidation(v: FieldValidation): FieldRules { const rules: FieldRules = {} if (v.required !== undefined) rules.required = v.required if (v.minLength !== undefined) rules.minLength = v.minLength if (v.maxLength !== undefined) rules.maxLength = v.maxLength if (v.min !== undefined) rules.min = v.min if (v.max !== undefined) rules.max = v.max if (v.email !== undefined) rules.email = v.email if (v.pattern !== undefined) { rules.pattern = typeof v.pattern === 'string' ? new RegExp(v.pattern) : { value: new RegExp(v.pattern.value), message: v.pattern.message } } return rules } export function buildInitialValues>( fields: readonly FormFieldSchema[], initialData?: Partial, ): Record { const initialValues = {} as Record for (const field of fields) { const key = field.name if (initialData && hasOwnKey(initialData, key)) { initialValues[key] = (initialData as Record)[key] } else if (field.defaultValue !== undefined) { initialValues[key] = field.defaultValue } else { initialValues[key] = getTypeDefault(field.type) } } return initialValues } export function buildRules>( fields: readonly FormFieldSchema[], enhancements?: FormEnhancements, ): Partial> { const rules: Partial> = {} for (const field of fields) { const base: FieldRules = field.validation ? convertValidation(field.validation) : {} const enhancement = enhancements?.fields?.[field.name as keyof T] const customValidators: Array<( value: unknown, formData: Record, ) => string | undefined | null> = [] if (enhancement?.validate) { const fn = enhancement.validate customValidators.push((value, formData) => fn(value, formData as T)) } if (customValidators.length > 0) { base.custom = customValidators } if (Object.keys(base).length > 0) { rules[field.name] = base } } return rules } export function replaceArray(target: T[], values: readonly T[]): void { target.splice(0, target.length, ...values) } export function replaceRecord( target: Partial>, source: Partial>, ): void { for (const key of Object.keys(target)) { delete target[key] } Object.assign(target, source) } function hasOwnKey(source: object, key: string): boolean { return Object.prototype.hasOwnProperty.call(source, key) }