use crate::value::Value;
use cel_interpreter::objects::{Key, Map as CelMap, Value as CelValue};
use cel_interpreter::{Context, ExecutionError, Program};
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CelError {
DivByZero,
Type(String),
Other(String),
}
impl std::fmt::Display for CelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CelError::DivByZero => write!(f, "division by zero"),
CelError::Type(m) => write!(f, "type error: {m}"),
CelError::Other(m) => write!(f, "{m}"),
}
}
}
impl std::error::Error for CelError {}
#[derive(Debug, Clone, Default)]
pub struct Env {
pub bindings: BTreeMap<String, Value>,
}
impl Env {
pub fn new() -> Self {
Self {
bindings: BTreeMap::new(),
}
}
pub fn with(mut self, k: impl Into<String>, v: Value) -> Self {
self.bindings.insert(k.into(), v);
self
}
pub fn get(&self, k: &str) -> Option<&Value> {
self.bindings.get(k)
}
}
pub fn eval(src: &str, env: &Env) -> Result<Value, CelError> {
let program = Program::compile(src)
.map_err(|e| CelError::Other(format!("CEL parse error: {e}")))?;
let mut ctx = Context::default();
for (k, v) in &env.bindings {
let _ = ctx.add_variable(k.clone(), to_cel(v));
}
let result = program.execute(&ctx).map_err(map_exec_error)?;
Ok(from_cel(&result))
}
pub fn eval_bool(src: &str, env: &Env) -> Result<bool, CelError> {
match eval(src, env)? {
Value::Bool(b) => Ok(b),
other => Ok(other.truthy()),
}
}
fn map_exec_error(e: ExecutionError) -> CelError {
match e {
ExecutionError::DivisionByZero(_) | ExecutionError::RemainderByZero(_) => CelError::DivByZero,
ExecutionError::UnexpectedType { .. } => CelError::Type(e.to_string()),
other => CelError::Other(other.to_string()),
}
}
fn to_cel(v: &Value) -> CelValue {
match v {
Value::Null => CelValue::Null,
Value::Bool(b) => CelValue::Bool(*b),
Value::Int(i) => CelValue::Int(*i),
Value::Float(f) => CelValue::Float(*f),
Value::Str(s) => CelValue::String(Arc::new(s.clone())),
Value::List(l) => {
CelValue::List(Arc::new(l.iter().map(to_cel).collect()))
}
Value::Map(m) => {
let mut hm: HashMap<String, CelValue> = HashMap::new();
for (k, vv) in m {
hm.insert(k.clone(), to_cel(vv));
}
CelValue::Map(CelMap::from(hm))
}
}
}
fn from_cel(v: &CelValue) -> Value {
match v {
CelValue::Null => Value::Null,
CelValue::Bool(b) => Value::Bool(*b),
CelValue::Int(i) => Value::Int(*i),
CelValue::UInt(u) => Value::Int(*u as i64),
CelValue::Float(f) => Value::Float(*f),
CelValue::String(s) => Value::Str(s.as_ref().clone()),
CelValue::List(l) => Value::List(l.iter().map(from_cel).collect()),
CelValue::Map(m) => {
let mut out = BTreeMap::new();
for (k, vv) in m.map.iter() {
out.insert(key_to_string(k), from_cel(vv));
}
Value::Map(out)
}
CelValue::Bytes(_) | CelValue::Duration(_) | CelValue::Timestamp(_) | CelValue::Function(..) => {
Value::Null
}
}
}
fn key_to_string(k: &Key) -> String {
match k {
Key::String(s) => s.as_ref().clone(),
Key::Int(i) => i.to_string(),
Key::Bool(b) => b.to_string(),
other => format!("{other:?}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env() -> Env {
Env::new()
.with("x", Value::Int(10))
.with(
"event",
Value::Map(
[(
"payload".to_string(),
Value::Map([("n".to_string(), Value::Int(5))].into_iter().collect()),
)]
.into_iter()
.collect(),
),
)
.with("s", Value::Str("hello".into()))
.with("items", Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]))
}
#[test]
fn basics() {
assert_eq!(eval("1 + 2", &Env::new()).unwrap(), Value::Int(3));
assert_eq!(eval("x + 5", &env()).unwrap(), Value::Int(15));
assert_eq!(eval("event.payload.n > 3", &env()).unwrap(), Value::Bool(true));
assert_eq!(eval("event.payload.n > 10", &env()).unwrap(), Value::Bool(false));
assert_eq!(eval("s + '!' == \"hello!\"", &env()).unwrap(), Value::Bool(true));
assert_eq!(eval("'a' in ['a','b']", &Env::new()).unwrap(), Value::Bool(true));
}
#[test]
fn div_zero() {
assert!(matches!(eval("10 / 0", &Env::new()), Err(CelError::DivByZero)));
}
#[test]
fn list_concat_and_map_literal() {
assert_eq!(
eval("[1,2] + ['c']", &Env::new()).unwrap(),
Value::List(vec![Value::Int(1), Value::Int(2), Value::Str("c".into())])
);
let m = eval("{'k': 2}", &Env::new()).unwrap();
assert_eq!(m, Value::Map([("k".to_string(), Value::Int(2))].into_iter().collect()));
}
#[test]
fn ternary() {
assert_eq!(eval("x > 0 ? 1 : 2", &env()).unwrap(), Value::Int(1));
let e = Env::new().with("x", Value::Int(-3));
assert_eq!(eval("x > 0 ? 1 : 2", &e).unwrap(), Value::Int(2));
}
#[test]
fn exists_macro() {
assert_eq!(eval("items.exists(i, i > 2)", &env()).unwrap(), Value::Bool(true));
assert_eq!(eval("items.exists(i, i > 9)", &env()).unwrap(), Value::Bool(false));
assert_eq!(eval("items.all(i, i > 0)", &env()).unwrap(), Value::Bool(true));
}
#[test]
fn functions() {
assert_eq!(eval("size(items)", &env()).unwrap(), Value::Int(3));
assert_eq!(eval("s.contains(\"ell\")", &env()).unwrap(), Value::Bool(true));
assert_eq!(eval("s.startsWith(\"he\")", &env()).unwrap(), Value::Bool(true));
}
}