import type { JsonLogicValue } from '@/jslg/lib'; import type {Obj, Opt, Sec} from '@/vcen/lib/types'; import type { ScopeEntry, ConditionNode, ConditionError, CompareOp, LogicOp, Operand, ResolvedOpt } from '@/vcen-edit/lib/condition-editor.ts'; import { buildScope, compileCondition, parseCondition, validateCondition, detectTautology, makeLogicNode, makeCompareNode, replaceNode, removeNode, addChild, getValidOperators, } from '@/vcen-edit/lib/condition-editor.ts'; // ───────────────────────────────────────────────────────────────────────────── // STORE // ───────────────────────────────────────────────────────────────────────────── /** * Adaptador Svelte 5 para el editor de condiciones JsonLogic. * * Usa runas (`$state`, `$derived`) para que los componentes Svelte * reaccionen automáticamente a los cambios del árbol de condición. * * ## Uso * * ```svelte * * * {#if store.tautology.kind === 'always-true'} * Esta condición siempre es verdadera * {/if} * * * ``` */ export function createConditionBuilderStore( container: Obj | Sec, initial?: JsonLogicValue, catOpts: Opt[] = [], ) { // ── Estado reactivo ─────────────────────────────────────────────────────── let root = $state( initial ? parseCondition(initial) : makeLogicNode('and', []) ); const scope = $derived(buildScope(container, catOpts)); const errors = $derived( validateCondition(root, scope) ); const tautology = $derived(detectTautology(root)); const compiled = $derived( errors.length === 0 ? compileCondition(root) : initial ?? true ); const isValid = $derived(errors.length === 0); // ── Mutaciones ──────────────────────────────────────────────────────────── function setRoot(node: ConditionNode) { root = node; } /** Añade un CompareNode vacío como hijo del LogicNode con `parentId`. */ function addCompare(parentId: string) { const child = makeCompareNode('=='); root = addChild(root, parentId, child); } /** Añade un LogicNode (and/or) como hijo del LogicNode con `parentId`. */ function addLogic(parentId: string, op: LogicOp = 'and') { const child = makeLogicNode(op, []); root = addChild(root, parentId, child); } /** Elimina el nodo con `id`. Si el árbol queda vacío, resetea a and vacío. */ function remove(id: string) { const next = removeNode(root, id); root = next ?? makeLogicNode('and', []); } /** Reemplaza el operador de un CompareNode. */ function setOperator(id: string, op: CompareOp) { const current = findNode(root, id); if (!current || current.kind !== 'compare') return; root = replaceNode(root, id, { ...current, op }); } /** Reemplaza el operando izquierdo de un CompareNode. */ function setLeft(id: string, operand: Operand) { const current = findNode(root, id); if (!current || current.kind !== 'compare') return; root = replaceNode(root, id, { ...current, left: operand }); } /** Reemplaza el operando derecho de un CompareNode. */ function setRight(id: string, operand: Operand | [Operand, Operand]) { const current = findNode(root, id); if (!current || current.kind !== 'compare') return; root = replaceNode(root, id, { ...current, right: operand }); } /** Cambia el operador lógico (and ↔ or) de un LogicNode. */ function setLogicOp(id: string, op: LogicOp) { const current = findNode(root, id); if (!current || current.kind !== 'logic') return; root = replaceNode(root, id, { ...current, op }); } /** * Operadores válidos para el att referenciado por `varPath`. * Útil para poblar el selector de operador en la UI. */ function validOperatorsFor(varPath: string): CompareOp[] { const entry = scope.find(e => e.path === varPath); if (!entry) return ['==', '!=']; return getValidOperators(entry.domain); } /** * Opciones resueltas para el att referenciado por `varPath`. * Solo tiene sentido para atts con dominio `option/list`. * Devuelve `[]` para cualquier otro dominio. * * Útil para poblar el selector de valor literal en la UI * cuando el operando derecho debe ser un OptID. */ function optsFor(varPath: string): ResolvedOpt[] { const entry = scope.find(e => e.path === varPath); return entry?.resolvedOpts ?? []; } /** * Carga una condición existente en el editor (p.ej. al abrir una regla). */ function load(condition: JsonLogicValue) { root = parseCondition(condition); } /** Resetea el editor a un LogicNode and vacío. */ function reset() { root = makeLogicNode('and', []); } // ── API pública ─────────────────────────────────────────────────────────── return { // Estado (reactivo) get root() { return root; }, get scope() { return scope; }, get errors() { return errors; }, get tautology() { return tautology; }, get compiled() { return compiled; }, get isValid() { return isValid; }, // Mutaciones setRoot, addCompare, addLogic, remove, setOperator, setLeft, setRight, setLogicOp, validOperatorsFor, optsFor, load, reset, }; } export type ConditionBuilderStore = ReturnType; // ───────────────────────────────────────────────────────────────────────────── // INTERNAL HELPERS // ───────────────────────────────────────────────────────────────────────────── function findNode(root: ConditionNode, id: string): ConditionNode | null { if (root.id === id) return root; if (root.kind === 'logic') { for (const child of root.children) { const found = findNode(child, id); if (found) return found; } } return null; }