Skip to main content

determa_state/
cel.rs

1//! Guard / action-value evaluation via the [Common Expression Language][cel]
2//! (SPEC §6), backed by the [`cel-interpreter`][crate] crate (cel-rust).
3//!
4//! Determa State mandates CEL so a guard means the same thing in every runtime. This module
5//! is a thin adapter: it builds a CEL context from the Determa State scope (in-scope `esvs`
6//! + `event.payload.*` + the intrinsics `id`/`parent`/`state`), converts between
7//! Determa State's [`Value`] and CEL values, and maps CEL runtime errors onto [`CelError`]
8//! (which the runtime treats as an action fault, SPEC §5.10). Boundary values stay
9//! canonical native/JSON (§5.1): every CEL result is normalized back to a [`Value`].
10//!
11//! [cel]: https://cel.dev/
12
13use crate::value::Value;
14use cel_interpreter::objects::{Key, Map as CelMap, Value as CelValue};
15use cel_interpreter::{Context, ExecutionError, Program};
16use std::collections::{BTreeMap, HashMap};
17use std::sync::Arc;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum CelError {
21    DivByZero,
22    Type(String),
23    Other(String),
24}
25
26impl std::fmt::Display for CelError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            CelError::DivByZero => write!(f, "division by zero"),
30            CelError::Type(m) => write!(f, "type error: {m}"),
31            CelError::Other(m) => write!(f, "{m}"),
32        }
33    }
34}
35impl std::error::Error for CelError {}
36
37/// A CEL environment: the bindings visible to a guard / action value (resolved
38/// in-scope `esvs` plus the `event`/`id`/`parent` intrinsics).
39#[derive(Debug, Clone, Default)]
40pub struct Env {
41    pub bindings: BTreeMap<String, Value>,
42}
43
44impl Env {
45    pub fn new() -> Self {
46        Self {
47            bindings: BTreeMap::new(),
48        }
49    }
50    pub fn with(mut self, k: impl Into<String>, v: Value) -> Self {
51        self.bindings.insert(k.into(), v);
52        self
53    }
54    pub fn get(&self, k: &str) -> Option<&Value> {
55        self.bindings.get(k)
56    }
57}
58
59/// Evaluate a CEL expression, returning its value as a canonical Determa State [`Value`].
60pub fn eval(src: &str, env: &Env) -> Result<Value, CelError> {
61    let program = Program::compile(src)
62        .map_err(|e| CelError::Other(format!("CEL parse error: {e}")))?;
63    let mut ctx = Context::default();
64    for (k, v) in &env.bindings {
65        let _ = ctx.add_variable(k.clone(), to_cel(v));
66    }
67    let result = program.execute(&ctx).map_err(map_exec_error)?;
68    Ok(from_cel(&result))
69}
70
71/// Evaluate a CEL expression as a boolean guard.
72pub fn eval_bool(src: &str, env: &Env) -> Result<bool, CelError> {
73    match eval(src, env)? {
74        Value::Bool(b) => Ok(b),
75        other => Ok(other.truthy()),
76    }
77}
78
79fn map_exec_error(e: ExecutionError) -> CelError {
80    match e {
81        ExecutionError::DivisionByZero(_) | ExecutionError::RemainderByZero(_) => CelError::DivByZero,
82        ExecutionError::UnexpectedType { .. } => CelError::Type(e.to_string()),
83        other => CelError::Other(other.to_string()),
84    }
85}
86
87// --- value conversion ------------------------------------------------------
88
89/// Convert a Determa State [`Value`] into a CEL value for context binding.
90fn to_cel(v: &Value) -> CelValue {
91    match v {
92        Value::Null => CelValue::Null,
93        Value::Bool(b) => CelValue::Bool(*b),
94        Value::Int(i) => CelValue::Int(*i),
95        Value::Float(f) => CelValue::Float(*f),
96        Value::Str(s) => CelValue::String(Arc::new(s.clone())),
97        Value::List(l) => {
98            CelValue::List(Arc::new(l.iter().map(to_cel).collect()))
99        }
100        Value::Map(m) => {
101            let mut hm: HashMap<String, CelValue> = HashMap::new();
102            for (k, vv) in m {
103                hm.insert(k.clone(), to_cel(vv));
104            }
105            CelValue::Map(CelMap::from(hm))
106        }
107    }
108}
109
110/// Normalize a CEL result back into a canonical Determa State [`Value`] (§5.1).
111fn from_cel(v: &CelValue) -> Value {
112    match v {
113        CelValue::Null => Value::Null,
114        CelValue::Bool(b) => Value::Bool(*b),
115        CelValue::Int(i) => Value::Int(*i),
116        CelValue::UInt(u) => Value::Int(*u as i64),
117        CelValue::Float(f) => Value::Float(*f),
118        CelValue::String(s) => Value::Str(s.as_ref().clone()),
119        CelValue::List(l) => Value::List(l.iter().map(from_cel).collect()),
120        CelValue::Map(m) => {
121            let mut out = BTreeMap::new();
122            for (k, vv) in m.map.iter() {
123                out.insert(key_to_string(k), from_cel(vv));
124            }
125            Value::Map(out)
126        }
127        // Bytes/Duration/Timestamp/Function have no Determa State value type; surface as null.
128        CelValue::Bytes(_) | CelValue::Duration(_) | CelValue::Timestamp(_) | CelValue::Function(..) => {
129            Value::Null
130        }
131    }
132}
133
134fn key_to_string(k: &Key) -> String {
135    match k {
136        Key::String(s) => s.as_ref().clone(),
137        Key::Int(i) => i.to_string(),
138        Key::Bool(b) => b.to_string(),
139        other => format!("{other:?}"),
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn env() -> Env {
148        Env::new()
149            .with("x", Value::Int(10))
150            .with(
151                "event",
152                Value::Map(
153                    [(
154                        "payload".to_string(),
155                        Value::Map([("n".to_string(), Value::Int(5))].into_iter().collect()),
156                    )]
157                    .into_iter()
158                    .collect(),
159                ),
160            )
161            .with("s", Value::Str("hello".into()))
162            .with("items", Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]))
163    }
164
165    #[test]
166    fn basics() {
167        assert_eq!(eval("1 + 2", &Env::new()).unwrap(), Value::Int(3));
168        assert_eq!(eval("x + 5", &env()).unwrap(), Value::Int(15));
169        assert_eq!(eval("event.payload.n > 3", &env()).unwrap(), Value::Bool(true));
170        assert_eq!(eval("event.payload.n > 10", &env()).unwrap(), Value::Bool(false));
171        assert_eq!(eval("s + '!' == \"hello!\"", &env()).unwrap(), Value::Bool(true));
172        assert_eq!(eval("'a' in ['a','b']", &Env::new()).unwrap(), Value::Bool(true));
173    }
174
175    #[test]
176    fn div_zero() {
177        assert!(matches!(eval("10 / 0", &Env::new()), Err(CelError::DivByZero)));
178    }
179
180    #[test]
181    fn list_concat_and_map_literal() {
182        assert_eq!(
183            eval("[1,2] + ['c']", &Env::new()).unwrap(),
184            Value::List(vec![Value::Int(1), Value::Int(2), Value::Str("c".into())])
185        );
186        let m = eval("{'k': 2}", &Env::new()).unwrap();
187        assert_eq!(m, Value::Map([("k".to_string(), Value::Int(2))].into_iter().collect()));
188    }
189
190    // --- CEL features beyond the old hand-rolled subset ---
191
192    #[test]
193    fn ternary() {
194        assert_eq!(eval("x > 0 ? 1 : 2", &env()).unwrap(), Value::Int(1));
195        let e = Env::new().with("x", Value::Int(-3));
196        assert_eq!(eval("x > 0 ? 1 : 2", &e).unwrap(), Value::Int(2));
197    }
198
199    #[test]
200    fn exists_macro() {
201        assert_eq!(eval("items.exists(i, i > 2)", &env()).unwrap(), Value::Bool(true));
202        assert_eq!(eval("items.exists(i, i > 9)", &env()).unwrap(), Value::Bool(false));
203        assert_eq!(eval("items.all(i, i > 0)", &env()).unwrap(), Value::Bool(true));
204    }
205
206    #[test]
207    fn functions() {
208        assert_eq!(eval("size(items)", &env()).unwrap(), Value::Int(3));
209        assert_eq!(eval("s.contains(\"ell\")", &env()).unwrap(), Value::Bool(true));
210        assert_eq!(eval("s.startsWith(\"he\")", &env()).unwrap(), Value::Bool(true));
211    }
212}