Skip to main content

protovalidate_buffa/
cel.rs

1use std::borrow::Cow;
2use std::sync::{Arc, OnceLock};
3
4use cel_interpreter::extractors::This;
5use cel_interpreter::{Context, Program, Value};
6
7use crate::{FieldPath, Violation};
8
9pub struct CelConstraint {
10    pub id: &'static str,
11    pub message: &'static str,
12    pub expression: &'static str,
13    program: OnceLock<Program>,
14}
15
16pub trait AsCelValue {
17    fn as_cel_value(&self) -> Value;
18}
19
20/// Proto scalar / list → CEL value conversion, used by plugin-emitted `AsCelValue` impls.
21pub trait ToCelValue {
22    fn to_cel_value(&self) -> Value;
23}
24
25impl ToCelValue for String {
26    fn to_cel_value(&self) -> Value {
27        Value::String(self.clone().into())
28    }
29}
30
31impl ToCelValue for str {
32    fn to_cel_value(&self) -> Value {
33        Value::String(self.to_string().into())
34    }
35}
36
37impl ToCelValue for i32 {
38    fn to_cel_value(&self) -> Value {
39        Value::Int(i64::from(*self))
40    }
41}
42
43impl ToCelValue for i64 {
44    fn to_cel_value(&self) -> Value {
45        Value::Int(*self)
46    }
47}
48
49impl ToCelValue for u32 {
50    fn to_cel_value(&self) -> Value {
51        Value::UInt(u64::from(*self))
52    }
53}
54
55impl ToCelValue for u64 {
56    fn to_cel_value(&self) -> Value {
57        Value::UInt(*self)
58    }
59}
60
61impl ToCelValue for f32 {
62    fn to_cel_value(&self) -> Value {
63        Value::Float(f64::from(*self))
64    }
65}
66
67impl ToCelValue for f64 {
68    fn to_cel_value(&self) -> Value {
69        Value::Float(*self)
70    }
71}
72
73impl ToCelValue for bool {
74    fn to_cel_value(&self) -> Value {
75        Value::Bool(*self)
76    }
77}
78
79impl ToCelValue for Vec<u8> {
80    fn to_cel_value(&self) -> Value {
81        Value::Bytes(self.clone().into())
82    }
83}
84
85impl<T: AsCelValue> ToCelValue for Option<T> {
86    fn to_cel_value(&self) -> Value {
87        self.as_ref().map_or(Value::Null, AsCelValue::as_cel_value)
88    }
89}
90
91impl<T: ToCelValue> ToCelValue for Vec<T> {
92    fn to_cel_value(&self) -> Value {
93        Value::List(self.iter().map(ToCelValue::to_cel_value).collect::<Vec<_>>().into())
94    }
95}
96
97impl<T: AsCelValue + Default> ToCelValue for buffa::MessageField<T> {
98    fn to_cel_value(&self) -> Value {
99        self.as_option().map_or(Value::Null, AsCelValue::as_cel_value)
100    }
101}
102
103impl<E: buffa::Enumeration> ToCelValue for buffa::EnumValue<E> {
104    fn to_cel_value(&self) -> Value {
105        Value::Int(i64::from(self.to_i32()))
106    }
107}
108
109impl<K, V, S> ToCelValue for std::collections::HashMap<K, V, S>
110where
111    K: ToCelValue + std::hash::Hash + Eq + Clone + Into<cel_interpreter::objects::Key>,
112    V: ToCelValue,
113    S: std::hash::BuildHasher,
114{
115    fn to_cel_value(&self) -> Value {
116        let map: cel_interpreter::objects::Map = self
117            .iter()
118            .map(|(k, v)| (k.clone().into(), v.to_cel_value()))
119            .collect::<std::collections::HashMap<_, _>>()
120            .into();
121        Value::Map(map)
122    }
123}
124
125/// Generic adapter used by emitted impls: accepts anything that implements `ToCelValue`.
126pub fn to_cel_value<T: ToCelValue + ?Sized>(v: &T) -> Value {
127    v.to_cel_value()
128}
129
130impl CelConstraint {
131    #[must_use]
132    pub const fn new(id: &'static str, message: &'static str, expression: &'static str) -> Self {
133        Self { id, message, expression, program: OnceLock::new() }
134    }
135
136    /// Evaluates this CEL expression against `this` (bound as the `this` variable
137    /// inside the expression) plus a per-call-frozen `now` timestamp.
138    ///
139    /// # Errors
140    ///
141    /// Returns a [`Violation`] when the compiled CEL expression returns
142    /// `false`, returns a non-empty string, or produces a runtime error.
143    ///
144    /// # Panics
145    ///
146    /// Panics at first call if the CEL expression fails to compile (it is
147    /// baked in at codegen time, so a parse failure indicates a plugin bug).
148    pub fn eval<T: AsCelValue>(&self, this: &T) -> Result<(), Violation> {
149        let program = self.program.get_or_init(|| {
150            Program::compile(self.expression)
151                .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
152        });
153
154        let mut ctx = Context::default();
155        ctx.add_variable("this", this.as_cel_value())
156            .expect("cel: 'this' binding");
157        ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
158            .expect("cel: 'now' binding");
159        register_custom_functions(&mut ctx);
160
161        let result = program
162            .execute(&ctx)
163            .map_err(|e| self.violation(Cow::Owned(format!("cel runtime error: {e}"))))?;
164
165        match result {
166            Value::Bool(true) => Ok(()),
167            Value::String(s) if s.is_empty() => Ok(()),
168            Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
169            Value::String(s) => {
170                if self.message.is_empty() {
171                    Err(self.violation(Cow::Owned(s.to_string())))
172                } else {
173                    Err(self.violation(Cow::Borrowed(self.message)))
174                }
175            }
176            other => Err(self.violation(Cow::Owned(format!(
177                "cel returned non-bool/string: {other:?}"
178            )))),
179        }
180    }
181
182    fn violation(&self, message: Cow<'static, str>) -> Violation {
183        Violation {
184            field: FieldPath::default(),
185            rule: FieldPath::default(),
186            rule_id: Cow::Borrowed(self.id),
187            message,
188            for_key: false,
189        }
190    }
191}
192
193fn register_custom_functions(ctx: &mut Context<'_>) {
194    // String format checks. Signatures match the canonical cel-go `protovalidate`
195    // library. The rule helpers module owns the implementations.
196    //
197    // The `This<Arc<String>>` receiver pattern enables method-call syntax:
198    // `this.ref_id.isUuid()` — same pattern as `startsWith` / `endsWith` in the
199    // cel-interpreter standard library.
200    ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
201        crate::rules::string::is_uuid(&this)
202    });
203    // Additional custom fns (isEmail, isHostname, isUri, isIp, ...) land here as
204    // the rollout needs them. For v1 only isUuid is referenced by generated CEL.
205}