cala-cel-interpreter 0.24.1

An interpreter for the Common Expression Language (CEL)
Documentation
use std::sync::Arc;

use cel::{
    extractors::{Arguments, This},
    objects::Value,
    ExecutionError,
};
use chrono::{FixedOffset, NaiveDate, TimeZone, Utc};
use es_entity::clock::ClockHandle;

use crate::value::{CelDecimal, CelUuid};

type Result<T> = std::result::Result<T, ExecutionError>;

pub(crate) fn date(clock: ClockHandle, Arguments(args): Arguments) -> Result<Value> {
    let date = match args.as_slice() {
        [] => clock.now().date_naive(),
        [Value::String(s)] => NaiveDate::parse_from_str(s, "%Y-%m-%d")
            .map_err(|e| ExecutionError::function_error("date", e))?,
        [Value::Timestamp(ts)] => ts.date_naive(),
        [v] => {
            return Err(ExecutionError::function_error(
                "date",
                format!("cannot convert {v:?} to date"),
            ))
        }
        values => {
            return Err(ExecutionError::invalid_argument_count(1, values.len()));
        }
    };

    let dt = date.and_hms_opt(0, 0, 0).expect("midnight is valid");
    Ok(Value::Timestamp(
        FixedOffset::east_opt(0)
            .expect("UTC offset is valid")
            .from_utc_datetime(&dt),
    ))
}

pub(crate) fn uuid(Arguments(args): Arguments) -> Result<Value> {
    match args.as_slice() {
        [Value::String(s)] => {
            let id = s
                .parse()
                .map_err(|e| ExecutionError::function_error("uuid", format!("{e:?}")))?;
            Ok(Value::Opaque(Arc::new(CelUuid(id))))
        }
        [v] => Err(ExecutionError::function_error(
            "uuid",
            format!("cannot convert {v:?} to uuid"),
        )),
        values => Err(ExecutionError::invalid_argument_count(1, values.len())),
    }
}

pub(crate) fn decimal(Arguments(args): Arguments) -> Result<Value> {
    match args.as_slice() {
        [Value::Opaque(o)] if o.runtime_type_name() == "cala.Decimal" => {
            Ok(Value::Opaque(o.clone()))
        }
        [Value::String(s)] => {
            let decimal = s
                .parse()
                .map_err(|e| ExecutionError::function_error("decimal", format!("{e:?}")))?;
            Ok(Value::Opaque(Arc::new(CelDecimal(decimal))))
        }
        [Value::Int(i)] => Ok(Value::Opaque(Arc::new(CelDecimal((*i).into())))),
        [Value::UInt(u)] => Ok(Value::Opaque(Arc::new(CelDecimal((*u).into())))),
        [v] => Err(ExecutionError::function_error(
            "decimal",
            format!("cannot convert {v:?} to decimal"),
        )),
        values => Err(ExecutionError::invalid_argument_count(1, values.len())),
    }
}

pub(crate) fn decimal_add(Arguments(args): Arguments) -> Result<Value> {
    let (left, right) = decimal_binary_args(&args)?;
    let res = left
        .checked_add(right)
        .ok_or_else(|| ExecutionError::function_error("decimal.Add", "overflow"))?;
    Ok(Value::Opaque(Arc::new(CelDecimal(res))))
}

pub(crate) fn decimal_sub(Arguments(args): Arguments) -> Result<Value> {
    let (left, right) = decimal_binary_args(&args)?;
    let res = left
        .checked_sub(right)
        .ok_or_else(|| ExecutionError::function_error("decimal.Sub", "overflow"))?;
    Ok(Value::Opaque(Arc::new(CelDecimal(res))))
}

pub(crate) fn decimal_mul(Arguments(args): Arguments) -> Result<Value> {
    let (left, right) = decimal_binary_args(&args)?;
    let res = left
        .checked_mul(right)
        .ok_or_else(|| ExecutionError::function_error("decimal.Mul", "overflow"))?;
    Ok(Value::Opaque(Arc::new(CelDecimal(res))))
}

/// Three-way comparison: returns -1, 0 or 1 as an int, so it composes with
/// native int comparisons (e.g. `decimal.Cmp(a, b) > 0` for `a > b`).
pub(crate) fn decimal_cmp(Arguments(args): Arguments) -> Result<Value> {
    let (left, right) = decimal_binary_args(&args)?;
    Ok(Value::Int(match left.cmp(&right) {
        std::cmp::Ordering::Less => -1,
        std::cmp::Ordering::Equal => 0,
        std::cmp::Ordering::Greater => 1,
    }))
}

fn decimal_binary_args(args: &[Value]) -> Result<(rust_decimal::Decimal, rust_decimal::Decimal)> {
    match args {
        [left, right] => Ok((decimal_from_value(left)?, decimal_from_value(right)?)),
        values => Err(ExecutionError::invalid_argument_count(2, values.len())),
    }
}

pub(crate) fn timestamp_format(This(this): This<Value>, format: Arc<String>) -> Result<Value> {
    match this {
        Value::Timestamp(ts) => {
            // `DelayedFormat`'s `Display` impl returns `fmt::Error` for
            // invalid format specifiers, and `.to_string()` panics on that
            // ("a Display implementation returned an error unexpectedly").
            // Write manually so bad format strings surface as a CEL error
            // instead of panicking.
            let mut buf = String::new();
            std::fmt::Write::write_fmt(
                &mut buf,
                format_args!("{}", ts.with_timezone(&Utc).format(&format)),
            )
            .map_err(|_| {
                ExecutionError::function_error(
                    "format",
                    format!("invalid timestamp format string: '{format}'"),
                )
            })?;
            Ok(Value::String(buf.into()))
        }
        v => Err(ExecutionError::function_error(
            "format",
            format!("cannot format {v:?} as timestamp"),
        )),
    }
}

fn decimal_from_value(value: &Value) -> Result<rust_decimal::Decimal> {
    match value {
        Value::Opaque(o) if o.runtime_type_name() == "cala.Decimal" => {
            let decimal = o.downcast_ref::<CelDecimal>().ok_or_else(|| {
                ExecutionError::function_error("decimal", "failed to downcast decimal")
            })?;
            Ok(decimal.0)
        }
        Value::String(s) => s
            .parse()
            .map_err(|e| ExecutionError::function_error("decimal", format!("{e:?}"))),
        Value::Int(i) => Ok((*i).into()),
        Value::UInt(u) => Ok((*u).into()),
        v => Err(ExecutionError::function_error(
            "decimal",
            format!("cannot convert {v:?} to decimal"),
        )),
    }
}