You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
v/src/vcen-edit/stores/condition-builder.svelte.ts

209 lines
7.3 KiB

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
* <script>
* import { createConditionBuilderStore } from './condition-editor.svelte';
*
* const store = createConditionBuilderStore(obj, existingCondition);
* </script>
*
* {#if store.tautology.kind === 'always-true'}
* <Warning>Esta condición siempre es verdadera</Warning>
* {/if}
*
* <button onclick={() => store.addCompare(store.root.id)}>
* + Condición
* </button>
* ```
*/
export function createConditionBuilderStore(
container: Obj | Sec,
initial?: JsonLogicValue,
catOpts: Opt[] = [],
) {
// ── Estado reactivo ───────────────────────────────────────────────────────
let root = $state<ConditionNode>(
initial
? parseCondition(initial)
: makeLogicNode('and', [])
);
const scope = $derived<ScopeEntry[]>(buildScope(container, catOpts));
const errors = $derived<ConditionError[]>(
validateCondition(root, scope)
);
const tautology = $derived(detectTautology(root));
const compiled = $derived<JsonLogicValue>(
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<typeof createConditionBuilderStore>;
// ─────────────────────────────────────────────────────────────────────────────
// 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;
}

Powered by TurnKey Linux.