1use serde_json::Value;
18
19pub const MAX_CEL_EXPR: usize = 4096;
22
23pub const FEATURE_MSG: &str = "CEL expressions require the 'cel' build feature";
25
26pub fn compile_check(expr: &str) -> Result<(), String> {
29 if expr.trim().is_empty() {
30 return Err("empty CEL expression".into());
31 }
32 if expr.len() > MAX_CEL_EXPR {
33 return Err(format!(
34 "CEL expression is {} bytes (max {MAX_CEL_EXPR})",
35 expr.len()
36 ));
37 }
38 #[cfg(feature = "cel")]
39 {
40 imp::compile(expr).map(|_| ())
41 }
42 #[cfg(not(feature = "cel"))]
43 {
44 Err(FEATURE_MSG.into())
45 }
46}
47
48pub fn eval_bool(expr: &str, vars: &[(&str, &Value)]) -> Result<bool, String> {
52 #[cfg(feature = "cel")]
53 {
54 match imp::eval(expr, vars)? {
55 cel_interpreter::Value::Bool(b) => Ok(b),
56 other => Err(format!(
57 "CEL expression returned {:?}, want bool",
58 other.type_of()
59 )),
60 }
61 }
62 #[cfg(not(feature = "cel"))]
63 {
64 let _ = (expr, vars);
65 Err(FEATURE_MSG.into())
66 }
67}
68
69pub fn eval_value(expr: &str, vars: &[(&str, &Value)]) -> Result<Value, String> {
72 #[cfg(feature = "cel")]
73 {
74 imp::eval(expr, vars)?
75 .json()
76 .map_err(|e| format!("CEL result is not JSON-representable: {e}"))
77 }
78 #[cfg(not(feature = "cel"))]
79 {
80 let _ = (expr, vars);
81 Err(FEATURE_MSG.into())
82 }
83}
84
85pub fn vars_of(map: &std::collections::BTreeMap<String, Value>) -> Vec<(&str, &Value)> {
88 map.iter().map(|(k, v)| (k.as_str(), v)).collect()
89}
90
91#[cfg(feature = "cel")]
92mod imp {
93 use serde_json::Value;
94
95 pub fn compile(expr: &str) -> Result<cel_interpreter::Program, String> {
101 let owned = expr.to_string();
102 match std::panic::catch_unwind(move || cel_interpreter::Program::compile(&owned)) {
103 Ok(r) => r.map_err(|e| format!("CEL parse: {e}")),
104 Err(_) => Err("CEL parse: malformed expression (the parser rejected it)".into()),
105 }
106 }
107
108 fn to_cel(v: &Value) -> cel_interpreter::Value {
115 use cel_interpreter::Value as C;
116 use std::sync::Arc;
117 match v {
118 Value::Null => C::Null,
119 Value::Bool(b) => C::Bool(*b),
120 Value::Number(n) => {
121 if let Some(i) = n.as_i64() {
122 C::Int(i)
123 } else if let Some(f) = n.as_f64() {
124 C::Float(f)
125 } else {
126 C::Null
127 }
128 }
129 Value::String(s) => C::String(Arc::new(s.clone())),
130 Value::Array(a) => C::List(Arc::new(a.iter().map(to_cel).collect())),
131 Value::Object(o) => {
132 let map: std::collections::HashMap<String, C> =
133 o.iter().map(|(k, v)| (k.clone(), to_cel(v))).collect();
134 C::Map(map.into())
135 }
136 }
137 }
138
139 pub fn eval(expr: &str, vars: &[(&str, &Value)]) -> Result<cel_interpreter::Value, String> {
140 let program = compile(expr)?;
141 let mut ctx = cel_interpreter::Context::default();
142 for (name, value) in vars {
143 ctx.add_variable_from_value(name.to_string(), to_cel(value));
144 }
145 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| program.execute(&ctx))) {
148 Ok(r) => r.map_err(|e| format!("CEL eval: {e}")),
149 Err(_) => Err("CEL eval: the interpreter failed on this expression".into()),
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use serde_json::json;
158
159 #[test]
160 fn empty_and_oversized_expressions_are_refused() {
161 assert!(compile_check("").is_err());
162 assert!(compile_check(&"1 + ".repeat(2000)).is_err());
163 }
164
165 #[cfg(feature = "cel")]
166 mod with_cel {
167 use super::*;
168
169 #[test]
170 fn compile_check_accepts_valid_and_names_parse_errors() {
171 assert!(compile_check("a.b >= 3 && c in ['x','y']").is_ok());
172 let e = compile_check("a >=< 3").unwrap_err();
173 assert!(e.contains("CEL parse"), "{e}");
174 }
175
176 #[test]
177 fn eval_bool_computes_arithmetic_and_macros_over_variables() {
178 let a = json!({"count": 7, "items": [{"s": "ok"}, {"s": "bad"}]});
179 let b = json!({"limit": 5});
180 let vars = vec![("a", &a), ("b", &b)];
181 assert!(eval_bool("a.count + 1 > b.limit * 1", &vars).unwrap());
182 assert!(eval_bool("a.items.exists(i, i.s == 'bad')", &vars).unwrap());
183 assert!(eval_bool("a.items.filter(i, i.s == 'ok').size() == 1", &vars).unwrap());
184 assert!(eval_bool("a.count", &vars).is_err());
186 assert!(eval_bool("ghost > 1", &vars).is_err());
188 }
189
190 #[test]
191 fn eval_value_shapes_json() {
192 let scan = json!({"items": [{"id": 1, "ok": true}, {"id": 2, "ok": false}, {"id": 3, "ok": true}]});
193 let vars = vec![("scan", &scan)];
194 let v = eval_value("scan.items.filter(i, i.ok).map(i, i.id)", &vars).unwrap();
195 assert_eq!(v, json!([1, 3]));
196 let v = eval_value(
197 "{'total': scan.items.size(), 'first': scan.items[0].id}",
198 &vars,
199 )
200 .unwrap();
201 assert_eq!(v, json!({"total": 3, "first": 1}));
202 }
203 }
204
205 #[cfg(not(feature = "cel"))]
206 #[test]
207 fn without_the_feature_every_entry_point_names_it() {
208 let v = json!(1);
209 let vars = vec![("a", &v)];
210 assert!(compile_check("a > 0").unwrap_err().contains("'cel'"));
211 assert!(eval_bool("a > 0", &vars).unwrap_err().contains("'cel'"));
212 assert!(eval_value("a", &vars).unwrap_err().contains("'cel'"));
213 }
214}