dwara_core/cel/
mod.rs

1//! CEL (Common Expression Language) engine (DW-058).
2//!
3//! This module provides a thin wrapper around the `cel-interpreter`
4//! crate, offering:
5//!
6//! - [`CelProgram`] — a compiled CEL program (parse + type-check done
7//!   at compile time, never on the request path).
8//! - [`CelContext`] — a CEL evaluation context (variable bindings).
9//! - `CelValue` — a CEL value (the result of evaluation).
10//!
11//! ## Design (decision 5; §9.3)
12//!
13//! CEL expressions are compiled once at config publish time and
14//! embedded in the snapshot as [`CelProgram`] instances. The request
15//! path only evaluates — it never parses or compiles. This keeps the
16//! hot path fast (the cel-interpreter's tree-walking evaluator is
17//! ~100-500 ns/op for simple expressions, measured by the benchmark
18//! in `benches/micro.rs`).
19//!
20//! ## AOT vs JIT
21//!
22//! The issue scope mentions "cranelift JIT optional". The current
23//! implementation uses the cel-interpreter's tree-walking evaluator
24//! (no JIT). If the benchmark shows this is too slow for a given use
25//! case, a cranelift JIT backend can be added behind a feature flag
26//! without changing the API — [`CelProgram`] would hold either an AST
27//! or JIT-compiled code, and [`CelProgram::evaluate`] would dispatch
28//! accordingly.
29//!
30//! ## Feature gate
31//!
32//! The `cel` cargo feature must be enabled. Without it, the module is
33//! not compiled and config fields that reference CEL expressions are
34//! accepted but inert.
35
36use std::collections::HashMap;
37
38pub use cel_interpreter::{ExecutionError, ParseError, ParseErrors, Value};
39
40// DW-059: CEL everywhere -- one CEL surface across four use-sites.
41pub mod everywhere;
42// PERF-09 (#260): CEL JIT compilation via cranelift. Optional; when
43// the `cel-jit` feature is ON, simple CEL expressions can be
44// JIT-compiled to native code. Falls back to the tree-walking
45// interpreter for complex expressions.
46pub mod jit;
47
48/// A compiled CEL program.
49///
50/// Created at config publish time by [`CelProgram::compile`]. The
51/// program is immutable and can be safely shared across threads (the
52/// underlying AST is `Send + Sync`).
53///
54/// On the request path, call [`CelProgram::evaluate`] with a
55/// [`CelContext`] containing the request variables.
56#[derive(Debug)]
57pub struct CelProgram {
58    source: String,
59    program: cel_interpreter::Program,
60}
61
62/// A CEL evaluation context — variable bindings for evaluation.
63///
64/// Built per-request (or per-evaluation) with the variables the CEL
65/// expression can reference. For example, a route condition expression
66/// `request.path.startsWith("/api/")` needs a `request` variable with
67/// a `path` field.
68pub struct CelContext {
69    inner: cel_interpreter::Context<'static>,
70}
71
72/// The result of evaluating a CEL expression.
73pub type CelResult = Result<Value, ExecutionError>;
74
75impl CelProgram {
76    /// Compile a CEL expression from source.
77    ///
78    /// This is the parse + type-check step. It should be called at
79    /// config publish time, never on the request path.
80    pub fn compile(source: &str) -> Result<Self, ParseErrors> {
81        let program = cel_interpreter::Program::compile(source)?;
82        Ok(Self {
83            source: source.to_string(),
84            program,
85        })
86    }
87
88    /// The original source string.
89    pub fn source(&self) -> &str {
90        &self.source
91    }
92
93    /// Evaluate the program against a context.
94    ///
95    /// This is the hot-path call. It walks the AST and resolves
96    /// variables/functions against the context. No parsing or
97    /// compilation happens here.
98    pub fn evaluate(&self, context: &CelContext) -> CelResult {
99        self.program.execute(&context.inner)
100    }
101
102    /// Returns the variables referenced by the program.
103    /// Useful for validation (e.g. ensuring an expression only
104    /// references allowed variables).
105    pub fn references(&self) -> Vec<String> {
106        self.program
107            .references()
108            .variables()
109            .into_iter()
110            .map(String::from)
111            .collect()
112    }
113}
114
115impl CelContext {
116    /// Create a new empty context.
117    pub fn new() -> Self {
118        Self {
119            inner: cel_interpreter::Context::default(),
120        }
121    }
122
123    /// Add a variable binding to the context.
124    ///
125    /// The value must implement `serde::Serialize` (the cel-interpreter
126    /// converts it to its internal `Value` type). Common types: strings,
127    /// integers, floats, bools, Vec, HashMap.
128    pub fn add_var<T: serde::Serialize>(&mut self, name: &str, value: &T) -> Result<(), String> {
129        self.inner
130            .add_variable(name, value)
131            .map_err(|e| format!("CEL context add_var {name}: {e}"))
132    }
133
134    /// Add a raw CEL Value to the context.
135    pub fn add_value(&mut self, name: &str, value: Value) {
136        self.inner.add_variable_from_value(name, value);
137    }
138
139    /// Add all key-value pairs from a HashMap to the context.
140    pub fn add_vars<T: serde::Serialize>(
141        &mut self,
142        vars: &HashMap<String, T>,
143    ) -> Result<(), String> {
144        for (key, value) in vars {
145            self.add_var(key, value)?;
146        }
147        Ok(())
148    }
149}
150
151impl Default for CelContext {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157/// Convert a CEL Value to a Rust bool, if possible.
158pub fn value_to_bool(value: &Value) -> Option<bool> {
159    match value {
160        Value::Bool(b) => Some(*b),
161        _ => None,
162    }
163}
164
165/// Convert a CEL Value to a Rust string, if possible.
166pub fn value_to_string(value: &Value) -> Option<String> {
167    match value {
168        Value::String(s) => Some(s.to_string()),
169        _ => None,
170    }
171}
172
173/// Convert a CEL Value to a Rust i64, if possible.
174pub fn value_to_int(value: &Value) -> Option<i64> {
175    match value {
176        Value::Int(i) => Some(*i),
177        Value::UInt(u) => Some(*u as i64),
178        _ => None,
179    }
180}
181
182/// Convert a CEL Value to a Rust f64, if possible.
183pub fn value_to_float(value: &Value) -> Option<f64> {
184    match value {
185        Value::Float(f) => Some(*f),
186        Value::Int(i) => Some(*i as f64),
187        Value::UInt(u) => Some(*u as f64),
188        _ => None,
189    }
190}