gioser

tawasuyu/brahman

Rust 478TOML 190Markdown 50JSON 176650 Definiciones

⎇ b83d40a833f70e0f2345993e1d41add5588d4544 crates/runtime/arje-brain/src/rules.rs Rust · 206 líneas · 6.4 KB Historia Quién tocó qué crudo

refactor(naming): A1 — ente→arje, vista→revista, pluma→fana b83d40a83 · sergio ·

1//! Tipos de regla. La fuente de verdad del shape es esta definición Rust;
2//! `schema/rule.k` queda como referencia de diseño no cargada.
3//!
4//! Cargables desde JSON (array, objeto-con-array, o JSONL). El motor no
5//! acepta Rules construidas a mano sin pasar por validate() — ver
6//! `engine::insert`.
7
8use arje_card::Capability;
9use serde::{Deserialize, Serialize};
10use ulid::Ulid;
11
12/// Triplet [Sujeto + Evento + Acción]. Inmutable tras carga.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Rule {
15    pub id: Ulid,
16    #[serde(default = "default_priority")]
17    pub priority: u8,
18    pub when: EventPattern,
19    pub then: Vec<Action>,
20    #[serde(default)]
21    pub scope: Scope,
22}
23
24fn default_priority() -> u8 { 5 }
25
26impl Rule {
27    pub fn validate(&self) -> Result<(), RuleError> {
28        if self.then.is_empty() {
29            return Err(RuleError::EmptyActions);
30        }
31        self.when.validate_recursive()
32    }
33}
34
35#[derive(Debug)]
36pub enum RuleError {
37    EmptyActions,
38    EmptySequence,
39    EmptyCompound,
40    InvalidPriority,
41}
42
43impl std::fmt::Display for RuleError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::EmptyActions => write!(f, "regla sin acciones"),
47            Self::EmptySequence => write!(f, "Sequence pattern con kinds vacío"),
48            Self::EmptyCompound => write!(f, "Either/All con patterns vacío"),
49            Self::InvalidPriority => write!(f, "prioridad fuera de rango"),
50        }
51    }
52}
53
54impl std::error::Error for RuleError {}
55
56/// Match del sujeto. Vacío en todos los campos = match cualquier Ente.
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58pub struct Scope {
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub subject_id: Option<Ulid>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub subject_label: Option<String>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub subject_has_cap: Option<Capability>,
65}
66
67impl Scope {
68    pub fn is_wildcard(&self) -> bool {
69        self.subject_id.is_none()
70            && self.subject_label.is_none()
71            && self.subject_has_cap.is_none()
72    }
73}
74
75/// Patrón de evento que dispara una regla. Tagged union — JSON con campo
76/// `type`. Soporta composición recursiva (Either/All) sobre Single y
77/// Sequence atómicos.
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
79#[serde(tag = "type")]
80pub enum EventPattern {
81    /// Match un único evento por kind.
82    Single { kind: EventKind },
83    /// Match si los últimos N eventos del history coinciden en orden con
84    /// `kinds`, todos dentro de `within_ms` (0 = sin límite temporal).
85    Sequence {
86        kinds: Vec<EventKind>,
87        #[serde(default)]
88        within_ms: u64,
89    },
90    /// OR: match si cualquier sub-pattern matchea.
91    Either { patterns: Vec<EventPattern> },
92    /// AND: match si todos los sub-patterns matchean simultáneamente
93    /// contra el mismo (event, history).
94    All { patterns: Vec<EventPattern> },
95}
96
97impl EventPattern {
98    /// `true` si el pattern es atómico (no recursivo) y puede ser indexado
99    /// por discriminante de `EventKind` para dispatch O(1). Compuestos
100    /// (Either/All) se evalúan en un bucket de fallback.
101    pub fn is_simple(&self) -> bool {
102        matches!(self, Self::Single { .. } | Self::Sequence { .. })
103    }
104
105    /// Última `EventKind` que dispara la evaluación de un pattern atómico.
106    /// Devuelve None para compuestos.
107    pub fn trigger_kind(&self) -> Option<&EventKind> {
108        match self {
109            Self::Single { kind } => Some(kind),
110            Self::Sequence { kinds, .. } => kinds.last(),
111            Self::Either { .. } | Self::All { .. } => None,
112        }
113    }
114
115    /// Validación recursiva. Compuestos vacíos se rechazan al cargar.
116    pub fn validate_recursive(&self) -> Result<(), RuleError> {
117        match self {
118            Self::Single { .. } => Ok(()),
119            Self::Sequence { kinds, .. } => {
120                if kinds.is_empty() { Err(RuleError::EmptySequence) } else { Ok(()) }
121            }
122            Self::Either { patterns } | Self::All { patterns } => {
123                if patterns.is_empty() {
124                    return Err(RuleError::EmptyCompound);
125                }
126                for p in patterns { p.validate_recursive()?; }
127                Ok(())
128            }
129        }
130    }
131}
132
133/// Tipo de evento que dispara reglas. Indexado por discriminante en el motor.
134/// Diseñado para que `EventKindDiscriminant::from(&kind)` sea barato (no hash
135/// del payload, sólo del tag).
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
137pub enum EventKind {
138    EnteSpawned,
139    EnteDied,
140    BusAnnounce,
141    BusInvoke,
142    BusInvokeOf(Capability),
143    DeviceAdded,
144    DeviceRemoved,
145    Custom(String),
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "lowercase")]
150pub enum LogLevel { Trace, Debug, Info, Warn, Error }
151
152impl LogLevel {
153    pub fn as_str(&self) -> &'static str {
154        match self {
155            Self::Trace => "trace",
156            Self::Debug => "debug",
157            Self::Info => "info",
158            Self::Warn => "warn",
159            Self::Error => "error",
160        }
161    }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(tag = "kind", rename_all = "PascalCase")]
166pub enum Action {
167    Log {
168        #[serde(default = "default_log_level")]
169        level: LogLevel,
170        message: String,
171    },
172    Notify {
173        target_id: Ulid,
174        message: String,
175    },
176    /// `card_blob` es JSON del EntityCard codificado en base64. El motor lo
177    /// decodifica y forwarda como SpawnRequest al graph.
178    Spawn {
179        card_blob: String,
180    },
181    Invoke {
182        target_cap: Capability,
183        /// blob crudo (en JSON viaja como base64 vía `blob_b64`).
184        #[serde(with = "blob_b64")]
185        blob: Vec<u8>,
186    },
187    Inhibit {
188        reason: String,
189    },
190}
191
192fn default_log_level() -> LogLevel { LogLevel::Info }
193
194mod blob_b64 {
195    use base64::{engine::general_purpose::STANDARD, Engine};
196    use serde::{Deserialize, Deserializer, Serializer};
197
198    pub fn serialize<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
199        s.serialize_str(&STANDARD.encode(bytes))
200    }
201
202    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
203        let s = String::deserialize(d)?;
204        STANDARD.decode(&s).map_err(serde::de::Error::custom)
205    }
206}