//! Loader de reglas desde archivos `.k` vía subprocess al CLI de KCL. //! //! No usamos el SDK Rust de KCL para no arrastrar la dependencia de Go runtime //! ni cgo. El CLI `kcl` produce JSON validado contra el schema declarado //! en el propio `.k` — equivalente funcional al SDK con coste cero de compile. //! //! Si `kcl` no está en PATH, el caller decide: cargar JSON crudo (skip KCL), //! o fallar el boot. //! //! ## Formato esperado del .k file //! //! ```kcl //! import .rule # schema/rule.k //! //! rules: [Rule] = [ //! Rule { id = "...", priority = 5, when = ..., then = [...] }, //! ... //! ] //! ``` //! //! Salida tras `kcl run --format json`: `{"rules": [...]}`. El loader busca //! la primera array en el JSON (top-level o anidada un nivel) y la deserializa. use crate::rules::Rule; use ente_card::EntityCard; use std::path::Path; use std::process::Command; use tracing::{debug, info}; /// Detecta si `kcl` está disponible en PATH. Útil para degradar a JSON-only /// en entornos sin la toolchain. pub fn kcl_available() -> bool { Command::new("kcl") .arg("version") .output() .map(|o| o.status.success()) .unwrap_or(false) } /// Ejecuta `kcl run --format=json` y devuelve el JSON crudo. pub fn run_kcl(path: &Path) -> anyhow::Result { let output = Command::new("kcl") .arg("run") .arg(path) .arg("--format=json") .output() .map_err(|e| anyhow::anyhow!("invocando `kcl`: {e}. ¿Instalado en PATH?"))?; if !output.status.success() { anyhow::bail!( "kcl run {} falló: {}", path.display(), String::from_utf8_lossy(&output.stderr) ); } debug!(path = %path.display(), out_bytes = output.stdout.len(), "kcl run ok"); Ok(String::from_utf8(output.stdout)?) } /// Carga reglas desde un archivo `.k` o JSON. Discrimina por extensión: /// `.k` → invoca KCL, `.json` → directo. pub fn load_rules_file(path: &Path) -> anyhow::Result> { let raw = match path.extension().and_then(|e| e.to_str()) { Some("k") => { info!(path = %path.display(), "cargando reglas vía kcl"); run_kcl(path)? } _ => { info!(path = %path.display(), "cargando reglas como JSON crudo"); std::fs::read_to_string(path)? } }; extract_rules_from_json(&raw) } /// Extrae un `Vec` de JSON que puede ser: /// 1. Array directo: `[{...}, {...}]` /// 2. Object con un campo array: `{"rules": [...]}` pub fn extract_rules_from_json(raw: &str) -> anyhow::Result> { let v: serde_json::Value = serde_json::from_str(raw)?; let arr = match v { serde_json::Value::Array(_) => v, serde_json::Value::Object(map) => { map.into_values() .find(|x| x.is_array()) .ok_or_else(|| anyhow::anyhow!("JSON no contiene ningún array"))? } _ => anyhow::bail!("JSON debe ser array o object con campo array"), }; let rules: Vec = serde_json::from_value(arr)?; Ok(rules) } // ============================================================================ // Carga de Cards desde KCL/JSON. Cierra la "puerta genética": ninguna Card // se acepta sin pasar `validate()` extendido en ente-card. // ============================================================================ /// Carga una `EntityCard` desde un archivo `.k` (vía kcl run) o `.json`. /// Pasa por `EntityCard::validate()` antes de devolver — falla rápida. pub fn load_card_file(path: &Path) -> anyhow::Result { let raw = match path.extension().and_then(|e| e.to_str()) { Some("k") => { info!(path = %path.display(), "cargando Card vía kcl"); run_kcl(path)? } _ => { info!(path = %path.display(), "cargando Card como JSON crudo"); std::fs::read_to_string(path)? } }; let card = extract_card_from_json(&raw)?; card.validate() .map_err(|e| anyhow::anyhow!("Card inválida ({}): {e}", path.display()))?; Ok(card) } /// Extrae una `EntityCard` de JSON. Acepta: /// 1. Object directamente serializable como EntityCard /// 2. Object dict con un único valor que sea EntityCard (KCL output típico) pub fn extract_card_from_json(raw: &str) -> anyhow::Result { let v: serde_json::Value = serde_json::from_str(raw)?; // Intento 1: deserializar el value directamente. if let Ok(c) = serde_json::from_value::(v.clone()) { return Ok(c); } // Intento 2: si es dict, buscar el primer value que parsee como Card. if let serde_json::Value::Object(map) = v { for (_, vv) in map { if let Ok(c) = serde_json::from_value::(vv) { return Ok(c); } } } anyhow::bail!("JSON no contiene una EntityCard válida") }