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}