Skip to main content

cel_core/eval/
activation.rs

1//! Variable bindings for CEL evaluation.
2//!
3//! The `Activation` trait provides a way to resolve variable names to values
4//! during expression evaluation. Different implementations support various
5//! use cases like simple maps, hierarchical scopes, and lazy evaluation.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use super::Value;
11
12/// Trait for resolving variable bindings during evaluation.
13///
14/// An activation provides the values for variables referenced in CEL expressions.
15/// Implementations can support simple key-value lookup, hierarchical scopes,
16/// or lazy evaluation.
17pub trait Activation: Send + Sync {
18    /// Resolve a variable name to its value.
19    ///
20    /// Returns `None` if the variable is not defined in this activation.
21    fn resolve(&self, name: &str) -> Option<Value>;
22
23    /// Check if a variable is defined (present check for `has()` macro).
24    ///
25    /// Default implementation returns true if `resolve()` returns Some.
26    fn has(&self, name: &str) -> bool {
27        self.resolve(name).is_some()
28    }
29}
30
31/// A simple activation backed by a HashMap.
32#[derive(Debug, Clone, Default)]
33pub struct MapActivation {
34    bindings: HashMap<String, Value>,
35}
36
37impl MapActivation {
38    /// Create a new empty activation.
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Insert a binding.
44    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) {
45        self.bindings.insert(name.into(), value.into());
46    }
47
48    /// Remove a binding.
49    pub fn remove(&mut self, name: &str) -> Option<Value> {
50        self.bindings.remove(name)
51    }
52
53    /// Get the number of bindings.
54    pub fn len(&self) -> usize {
55        self.bindings.len()
56    }
57
58    /// Check if empty.
59    pub fn is_empty(&self) -> bool {
60        self.bindings.is_empty()
61    }
62}
63
64impl Activation for MapActivation {
65    fn resolve(&self, name: &str) -> Option<Value> {
66        self.bindings.get(name).cloned()
67    }
68
69    fn has(&self, name: &str) -> bool {
70        self.bindings.contains_key(name)
71    }
72}
73
74impl FromIterator<(String, Value)> for MapActivation {
75    fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
76        Self {
77            bindings: iter.into_iter().collect(),
78        }
79    }
80}
81
82/// A hierarchical activation that delegates to a parent if not found locally.
83///
84/// This is useful for implementing variable scopes in comprehensions
85/// where iteration variables shadow outer variables.
86pub struct HierarchicalActivation<'a> {
87    parent: &'a dyn Activation,
88    local: HashMap<String, Value>,
89}
90
91impl<'a> HierarchicalActivation<'a> {
92    /// Create a new hierarchical activation with a parent.
93    pub fn new(parent: &'a dyn Activation) -> Self {
94        Self {
95            parent,
96            local: HashMap::new(),
97        }
98    }
99
100    /// Add a local binding that shadows the parent.
101    pub fn with_binding(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
102        self.local.insert(name.into(), value.into());
103        self
104    }
105
106    /// Insert a local binding.
107    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) {
108        self.local.insert(name.into(), value.into());
109    }
110
111    /// Remove a local binding.
112    pub fn remove(&mut self, name: &str) -> Option<Value> {
113        self.local.remove(name)
114    }
115}
116
117impl Activation for HierarchicalActivation<'_> {
118    fn resolve(&self, name: &str) -> Option<Value> {
119        // Check local bindings first, then delegate to parent
120        self.local
121            .get(name)
122            .cloned()
123            .or_else(|| self.parent.resolve(name))
124    }
125
126    fn has(&self, name: &str) -> bool {
127        self.local.contains_key(name) || self.parent.has(name)
128    }
129}
130
131/// An empty activation with no bindings.
132///
133/// Useful as a default or when no variables are needed.
134#[derive(Debug, Clone, Copy, Default)]
135pub struct EmptyActivation;
136
137impl EmptyActivation {
138    /// Create a new empty activation.
139    pub fn new() -> Self {
140        Self
141    }
142}
143
144impl Activation for EmptyActivation {
145    fn resolve(&self, _name: &str) -> Option<Value> {
146        None
147    }
148
149    fn has(&self, _name: &str) -> bool {
150        false
151    }
152}
153
154/// An activation that wraps an Arc for shared ownership.
155#[derive(Clone)]
156pub struct SharedActivation {
157    inner: Arc<dyn Activation>,
158}
159
160impl std::fmt::Debug for SharedActivation {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.debug_struct("SharedActivation").finish_non_exhaustive()
163    }
164}
165
166impl SharedActivation {
167    /// Create a new shared activation.
168    pub fn new(activation: impl Activation + 'static) -> Self {
169        Self {
170            inner: Arc::new(activation),
171        }
172    }
173}
174
175impl Activation for SharedActivation {
176    fn resolve(&self, name: &str) -> Option<Value> {
177        self.inner.resolve(name)
178    }
179
180    fn has(&self, name: &str) -> bool {
181        self.inner.has(name)
182    }
183}
184
185impl<T: Activation> Activation for Arc<T> {
186    fn resolve(&self, name: &str) -> Option<Value> {
187        (**self).resolve(name)
188    }
189
190    fn has(&self, name: &str) -> bool {
191        (**self).has(name)
192    }
193}
194
195impl<T: Activation> Activation for Box<T> {
196    fn resolve(&self, name: &str) -> Option<Value> {
197        (**self).resolve(name)
198    }
199
200    fn has(&self, name: &str) -> bool {
201        (**self).has(name)
202    }
203}
204
205impl<T: Activation + ?Sized> Activation for &T {
206    fn resolve(&self, name: &str) -> Option<Value> {
207        (**self).resolve(name)
208    }
209
210    fn has(&self, name: &str) -> bool {
211        (**self).has(name)
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_map_activation() {
221        let mut activation = MapActivation::new();
222        activation.insert("x", 42i64);
223        activation.insert("name", "hello");
224
225        assert_eq!(activation.resolve("x"), Some(Value::Int(42)));
226        assert_eq!(activation.resolve("name"), Some(Value::from("hello")));
227        assert_eq!(activation.resolve("unknown"), None);
228
229        assert!(activation.has("x"));
230        assert!(!activation.has("unknown"));
231    }
232
233    #[test]
234    fn test_hierarchical_activation() {
235        let parent: MapActivation = [
236            ("x".to_string(), Value::Int(1)),
237            ("y".to_string(), Value::Int(2)),
238        ]
239        .into_iter()
240        .collect();
241
242        let child = HierarchicalActivation::new(&parent).with_binding("x", 10i64);
243
244        // Local binding shadows parent
245        assert_eq!(child.resolve("x"), Some(Value::Int(10)));
246        // Parent binding is accessible
247        assert_eq!(child.resolve("y"), Some(Value::Int(2)));
248        // Unknown still returns None
249        assert_eq!(child.resolve("z"), None);
250    }
251
252    #[test]
253    fn test_empty_activation() {
254        let activation = EmptyActivation::new();
255        assert_eq!(activation.resolve("anything"), None);
256        assert!(!activation.has("anything"));
257    }
258
259    #[test]
260    fn test_activation_insert_without_suffix() {
261        // This is the ergonomic improvement - no i64 suffix needed
262        let mut activation = MapActivation::new();
263        activation.insert("count", 42); // i32 default works now
264        activation.insert("small", 5i8);
265        activation.insert("medium", 1000i16);
266        activation.insert("len", vec![1u8, 2, 3].len()); // usize from .len()
267
268        assert_eq!(activation.resolve("count"), Some(Value::Int(42)));
269        assert_eq!(activation.resolve("small"), Some(Value::Int(5)));
270        assert_eq!(activation.resolve("medium"), Some(Value::Int(1000)));
271        assert_eq!(activation.resolve("len"), Some(Value::UInt(3)));
272    }
273}