use std::borrow::Cow;
use std::sync::{Arc, OnceLock};
use cel_interpreter::extractors::This;
use cel_interpreter::{Context, Program, Value};
use crate::{FieldPath, Violation};
pub struct CelConstraint {
pub id: &'static str,
pub message: &'static str,
pub expression: &'static str,
program: OnceLock<Program>,
}
pub trait AsCelValue {
fn as_cel_value(&self) -> Value;
}
pub trait ToCelValue {
fn to_cel_value(&self) -> Value;
}
impl ToCelValue for String {
fn to_cel_value(&self) -> Value {
Value::String(self.clone().into())
}
}
impl ToCelValue for str {
fn to_cel_value(&self) -> Value {
Value::String(self.to_string().into())
}
}
impl ToCelValue for i32 {
fn to_cel_value(&self) -> Value {
Value::Int(i64::from(*self))
}
}
impl ToCelValue for i64 {
fn to_cel_value(&self) -> Value {
Value::Int(*self)
}
}
impl ToCelValue for u32 {
fn to_cel_value(&self) -> Value {
Value::UInt(u64::from(*self))
}
}
impl ToCelValue for u64 {
fn to_cel_value(&self) -> Value {
Value::UInt(*self)
}
}
impl ToCelValue for f32 {
fn to_cel_value(&self) -> Value {
Value::Float(f64::from(*self))
}
}
impl ToCelValue for f64 {
fn to_cel_value(&self) -> Value {
Value::Float(*self)
}
}
impl ToCelValue for bool {
fn to_cel_value(&self) -> Value {
Value::Bool(*self)
}
}
impl ToCelValue for Vec<u8> {
fn to_cel_value(&self) -> Value {
Value::Bytes(self.clone().into())
}
}
impl<T: AsCelValue> ToCelValue for Option<T> {
fn to_cel_value(&self) -> Value {
self.as_ref().map_or(Value::Null, AsCelValue::as_cel_value)
}
}
impl<T: ToCelValue> ToCelValue for Vec<T> {
fn to_cel_value(&self) -> Value {
Value::List(self.iter().map(ToCelValue::to_cel_value).collect::<Vec<_>>().into())
}
}
impl<T: AsCelValue + Default> ToCelValue for buffa::MessageField<T> {
fn to_cel_value(&self) -> Value {
self.as_option().map_or(Value::Null, AsCelValue::as_cel_value)
}
}
impl<E: buffa::Enumeration> ToCelValue for buffa::EnumValue<E> {
fn to_cel_value(&self) -> Value {
Value::Int(i64::from(self.to_i32()))
}
}
impl<K, V, S> ToCelValue for std::collections::HashMap<K, V, S>
where
K: ToCelValue + std::hash::Hash + Eq + Clone + Into<cel_interpreter::objects::Key>,
V: ToCelValue,
S: std::hash::BuildHasher,
{
fn to_cel_value(&self) -> Value {
let map: cel_interpreter::objects::Map = self
.iter()
.map(|(k, v)| (k.clone().into(), v.to_cel_value()))
.collect::<std::collections::HashMap<_, _>>()
.into();
Value::Map(map)
}
}
pub fn to_cel_value<T: ToCelValue + ?Sized>(v: &T) -> Value {
v.to_cel_value()
}
impl CelConstraint {
#[must_use]
pub const fn new(id: &'static str, message: &'static str, expression: &'static str) -> Self {
Self { id, message, expression, program: OnceLock::new() }
}
pub fn eval<T: AsCelValue>(&self, this: &T) -> Result<(), Violation> {
let program = self.program.get_or_init(|| {
Program::compile(self.expression)
.unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
});
let mut ctx = Context::default();
ctx.add_variable("this", this.as_cel_value())
.expect("cel: 'this' binding");
ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
.expect("cel: 'now' binding");
register_custom_functions(&mut ctx);
let result = program
.execute(&ctx)
.map_err(|e| self.violation(Cow::Owned(format!("cel runtime error: {e}"))))?;
match result {
Value::Bool(true) => Ok(()),
Value::String(s) if s.is_empty() => Ok(()),
Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
Value::String(s) => {
if self.message.is_empty() {
Err(self.violation(Cow::Owned(s.to_string())))
} else {
Err(self.violation(Cow::Borrowed(self.message)))
}
}
other => Err(self.violation(Cow::Owned(format!(
"cel returned non-bool/string: {other:?}"
)))),
}
}
fn violation(&self, message: Cow<'static, str>) -> Violation {
Violation {
field: FieldPath::default(),
rule: FieldPath::default(),
rule_id: Cow::Borrowed(self.id),
message,
for_key: false,
}
}
}
fn register_custom_functions(ctx: &mut Context<'_>) {
ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
crate::rules::string::is_uuid(&this)
});
}