gioser
⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229 crates/ente-brain/src/rules.rs Rust · 204 líneas · 6.3 KB Historia Quién tocó qué crudo

Pausa: 11 crates del fractal Ente #0 con cerebro completo d6b8f18b4 · Sergio ·

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