protovalidate_buffa/
cel.rs1use 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
20pub 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
125pub 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 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 ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
201 crate::rules::string::is_uuid(&this)
202 });
203 }