/** * Visibility engine for form sections and fields. * * Evaluates a VisibilityRule tree against the current form state. * Form state is a plain object {fieldName: value}. Missing fields are * treated as undefined. * * Rule semantics: * - SimpleCondition: tests a single field with an operator * - {all: [...]}: AND * - {any: [...]}: OR * * Operator semantics: * - equals : field value === rule.value * - notEquals : field value !== rule.value * - in : rule.values includes the field value * - notIn : rule.values does not include the field value * - isEmpty : field is undefined / null / "" / [] / {} * - isNotEmpty : negation of isEmpty * * If a rule is malformed, evaluate returns false (field stays hidden) rather * than throwing, so a config typo can never break the page. */ import type { VisibilityRule, SimpleCondition, AllCondition, AnyCondition, } from "@/types/form"; type FormState = Record; function isSimple(r: VisibilityRule): r is SimpleCondition { return typeof (r as SimpleCondition).field === "string"; } function isAll(r: VisibilityRule): r is AllCondition { return Array.isArray((r as AllCondition).all); } function isAny(r: VisibilityRule): r is AnyCondition { return Array.isArray((r as AnyCondition).any); } function isEmpty(v: unknown): boolean { if (v === undefined || v === null) return true; if (typeof v === "string") return v.length === 0; if (Array.isArray(v)) return v.length === 0; if (typeof v === "object") return Object.keys(v as object).length === 0; return false; } function evalSimple(c: SimpleCondition, state: FormState): boolean { const v = state[c.field]; switch (c.op) { case "equals": return v === c.value; case "notEquals": return v !== c.value; case "in": return Array.isArray(c.values) && c.values.includes(v as string | number | boolean); case "notIn": return Array.isArray(c.values) && !c.values.includes(v as string | number | boolean); case "isEmpty": return isEmpty(v); case "isNotEmpty": return !isEmpty(v); default: return false; } } export function evaluate(rule: VisibilityRule | undefined, state: FormState): boolean { if (!rule) return true; if (isSimple(rule)) return evalSimple(rule, state); if (isAll(rule)) return rule.all.every((r) => evaluate(r, state)); if (isAny(rule)) return rule.any.some((r) => evaluate(r, state)); return false; }