protovalidate-buffa 0.0.0

Runtime for protoc-gen-protovalidate-buffa — Validate trait, ValidationError types, CEL integration, rule helpers.
Documentation
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;
}

/// Proto scalar / list → CEL value conversion, used by plugin-emitted `AsCelValue` impls.
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)
    }
}

/// Generic adapter used by emitted impls: accepts anything that implements `ToCelValue`.
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() }
    }

    /// Evaluates this CEL expression against `this` (bound as the `this` variable
    /// inside the expression) plus a per-call-frozen `now` timestamp.
    ///
    /// # Errors
    ///
    /// Returns a [`Violation`] when the compiled CEL expression returns
    /// `false`, returns a non-empty string, or produces a runtime error.
    ///
    /// # Panics
    ///
    /// Panics at first call if the CEL expression fails to compile (it is
    /// baked in at codegen time, so a parse failure indicates a plugin bug).
    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<'_>) {
    // String format checks. Signatures match the canonical cel-go `protovalidate`
    // library. The rule helpers module owns the implementations.
    //
    // The `This<Arc<String>>` receiver pattern enables method-call syntax:
    // `this.ref_id.isUuid()` — same pattern as `startsWith` / `endsWith` in the
    // cel-interpreter standard library.
    ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
        crate::rules::string::is_uuid(&this)
    });
    // Additional custom fns (isEmail, isHostname, isUri, isIp, ...) land here as
    // the rollout needs them. For v1 only isUuid is referenced by generated CEL.
}