1use std::{
2 borrow::Cow,
3 sync::{Arc, OnceLock},
4};
5
6use cel_interpreter::{
7 extractors::{Arguments, This},
8 Context, Program, Value,
9};
10
11use crate::{FieldPath, Violation};
12
13pub struct CelConstraint {
14 pub id: &'static str,
15 pub message: &'static str,
16 pub expression: &'static str,
17 program: OnceLock<Program>,
18}
19
20pub trait AsCelValue {
21 fn as_cel_value(&self) -> Value;
22}
23
24pub trait ToCelValue {
26 fn to_cel_value(&self) -> Value;
27}
28
29impl ToCelValue for String {
30 fn to_cel_value(&self) -> Value {
31 Value::String(self.clone().into())
32 }
33}
34
35impl ToCelValue for str {
36 fn to_cel_value(&self) -> Value {
37 Value::String(self.to_string().into())
38 }
39}
40
41impl ToCelValue for i32 {
42 fn to_cel_value(&self) -> Value {
43 Value::Int(i64::from(*self))
44 }
45}
46
47impl ToCelValue for i64 {
48 fn to_cel_value(&self) -> Value {
49 Value::Int(*self)
50 }
51}
52
53impl ToCelValue for u32 {
54 fn to_cel_value(&self) -> Value {
55 Value::UInt(u64::from(*self))
56 }
57}
58
59impl ToCelValue for u64 {
60 fn to_cel_value(&self) -> Value {
61 Value::UInt(*self)
62 }
63}
64
65impl ToCelValue for f32 {
66 fn to_cel_value(&self) -> Value {
67 Value::Float(f64::from(*self))
68 }
69}
70
71impl ToCelValue for f64 {
72 fn to_cel_value(&self) -> Value {
73 Value::Float(*self)
74 }
75}
76
77impl ToCelValue for bool {
78 fn to_cel_value(&self) -> Value {
79 Value::Bool(*self)
80 }
81}
82
83impl ToCelValue for Vec<u8> {
84 fn to_cel_value(&self) -> Value {
85 Value::Bytes(self.clone().into())
86 }
87}
88
89impl<T: AsCelValue> ToCelValue for Option<T> {
90 fn to_cel_value(&self) -> Value {
91 self.as_ref().map_or(Value::Null, AsCelValue::as_cel_value)
92 }
93}
94
95impl<T: ToCelValue> ToCelValue for Vec<T> {
96 fn to_cel_value(&self) -> Value {
97 Value::List(
98 self.iter()
99 .map(ToCelValue::to_cel_value)
100 .collect::<Vec<_>>()
101 .into(),
102 )
103 }
104}
105
106impl<T: AsCelValue + Default> ToCelValue for buffa::MessageField<T> {
107 fn to_cel_value(&self) -> Value {
108 self.as_option()
109 .map_or(Value::Null, AsCelValue::as_cel_value)
110 }
111}
112
113impl<E: buffa::Enumeration> ToCelValue for buffa::EnumValue<E> {
114 fn to_cel_value(&self) -> Value {
115 Value::Int(i64::from(self.to_i32()))
116 }
117}
118
119macro_rules! impl_to_cel_for_hashmap_key {
120 ($kty:ty => $ktarget:ty) => {
121 impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
122 where
123 V: ToCelValue,
124 S: std::hash::BuildHasher,
125 {
126 fn to_cel_value(&self) -> Value {
127 let map: cel_interpreter::objects::Map = self
128 .iter()
129 .map(|(k, v)| {
130 (
131 cel_interpreter::objects::Key::from(k.clone() as $ktarget),
132 v.to_cel_value(),
133 )
134 })
135 .collect::<std::collections::HashMap<_, _>>()
136 .into();
137 Value::Map(map)
138 }
139 }
140 };
141 (string: $kty:ty) => {
142 impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
143 where
144 V: ToCelValue,
145 S: std::hash::BuildHasher,
146 {
147 fn to_cel_value(&self) -> Value {
148 let map: cel_interpreter::objects::Map = self
149 .iter()
150 .map(|(k, v)| {
151 (
152 cel_interpreter::objects::Key::from(k.clone()),
153 v.to_cel_value(),
154 )
155 })
156 .collect::<std::collections::HashMap<_, _>>()
157 .into();
158 Value::Map(map)
159 }
160 }
161 };
162}
163impl_to_cel_for_hashmap_key!(i32 => i64);
164impl_to_cel_for_hashmap_key!(u32 => u64);
165impl_to_cel_for_hashmap_key!(i64 => i64);
166impl_to_cel_for_hashmap_key!(u64 => u64);
167impl_to_cel_for_hashmap_key!(string: String);
168
169impl<V, S> ToCelValue for std::collections::HashMap<bool, V, S>
170where
171 V: ToCelValue,
172 S: std::hash::BuildHasher,
173{
174 fn to_cel_value(&self) -> Value {
175 let map: cel_interpreter::objects::Map = self
176 .iter()
177 .map(|(k, v)| (cel_interpreter::objects::Key::from(*k), v.to_cel_value()))
178 .collect::<std::collections::HashMap<_, _>>()
179 .into();
180 Value::Map(map)
181 }
182}
183
184pub fn enum_to_i32<E: buffa::Enumeration + Copy>(v: &E) -> i32 {
189 v.to_i32()
190}
191
192#[must_use]
193pub fn duration_from_secs_nanos(seconds: i64, nanos: i32) -> chrono::Duration {
194 chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(i64::from(nanos))
195}
196
197#[must_use]
202pub fn timestamp_from_secs_nanos(
203 seconds: i64,
204 nanos: i32,
205) -> chrono::DateTime<chrono::FixedOffset> {
206 let nanos_u32 = u32::try_from(nanos.max(0)).unwrap_or(0);
207 let s = chrono::DateTime::<chrono::Utc>::from_timestamp(seconds, nanos_u32)
208 .unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
209 s.fixed_offset()
210}
211
212pub fn to_cel_value<T: ToCelValue + ?Sized>(v: &T) -> Value {
213 v.to_cel_value()
214}
215
216impl CelConstraint {
217 #[must_use]
218 pub const fn new(id: &'static str, message: &'static str, expression: &'static str) -> Self {
219 Self {
220 id,
221 message,
222 expression,
223 program: OnceLock::new(),
224 }
225 }
226
227 pub fn eval_value_at(
245 &self,
246 this: Value,
247 field_path: FieldPath,
248 cel_index: u64,
249 ) -> Result<(), Violation> {
250 let r = self.eval_value(this);
251 match r {
252 Ok(()) => Ok(()),
253 Err(mut v) => {
254 v.field = field_path;
255 v.rule = FieldPath {
256 elements: vec![crate::FieldPathElement {
257 field_number: Some(23),
258 field_name: Some(Cow::Borrowed("cel")),
259 field_type: Some(crate::FieldType::Message),
260 key_type: None,
261 value_type: None,
262 subscript: Some(crate::Subscript::Index(cel_index)),
263 }],
264 };
265 Err(v)
266 }
267 }
268 }
269
270 pub fn eval_expr_value_at(
278 &self,
279 this: Value,
280 field_path: FieldPath,
281 index: u64,
282 ) -> Result<(), Violation> {
283 let r = self.eval_value(this);
284 match r {
285 Ok(()) => Ok(()),
286 Err(mut v) => {
287 v.field = field_path;
288 v.rule = FieldPath {
289 elements: vec![crate::FieldPathElement {
290 field_number: Some(29),
291 field_name: Some(Cow::Borrowed("cel_expression")),
292 field_type: Some(crate::FieldType::String),
293 key_type: None,
294 value_type: None,
295 subscript: Some(crate::Subscript::Index(index)),
296 }],
297 };
298 Err(v)
299 }
300 }
301 }
302
303 pub fn eval_repeated_items_cel(
310 &self,
311 this: Value,
312 field_path: FieldPath,
313 cel_idx: u64,
314 ) -> Result<(), Violation> {
315 let r = self.eval_value(this);
316 match r {
317 Ok(()) => Ok(()),
318 Err(mut v) => {
319 v.field = field_path;
320 v.rule = FieldPath {
321 elements: vec![
322 crate::FieldPathElement {
323 field_number: Some(18),
324 field_name: Some(Cow::Borrowed("repeated")),
325 field_type: Some(crate::FieldType::Message),
326 key_type: None,
327 value_type: None,
328 subscript: None,
329 },
330 crate::FieldPathElement {
331 field_number: Some(4),
332 field_name: Some(Cow::Borrowed("items")),
333 field_type: Some(crate::FieldType::Message),
334 key_type: None,
335 value_type: None,
336 subscript: None,
337 },
338 crate::FieldPathElement {
339 field_number: Some(23),
340 field_name: Some(Cow::Borrowed("cel")),
341 field_type: Some(crate::FieldType::Message),
342 key_type: None,
343 value_type: None,
344 subscript: Some(crate::Subscript::Index(cel_idx)),
345 },
346 ],
347 };
348 Err(v)
349 }
350 }
351 }
352
353 pub fn eval_map_keys_cel(
359 &self,
360 this: Value,
361 field_path: FieldPath,
362 cel_idx: u64,
363 ) -> Result<(), Violation> {
364 let r = self.eval_value(this);
365 match r {
366 Ok(()) => Ok(()),
367 Err(mut v) => {
368 v.field = field_path;
369 v.for_key = true;
370 v.rule = FieldPath {
371 elements: vec![
372 crate::FieldPathElement {
373 field_number: Some(19),
374 field_name: Some(Cow::Borrowed("map")),
375 field_type: Some(crate::FieldType::Message),
376 key_type: None,
377 value_type: None,
378 subscript: None,
379 },
380 crate::FieldPathElement {
381 field_number: Some(4),
382 field_name: Some(Cow::Borrowed("keys")),
383 field_type: Some(crate::FieldType::Message),
384 key_type: None,
385 value_type: None,
386 subscript: None,
387 },
388 crate::FieldPathElement {
389 field_number: Some(23),
390 field_name: Some(Cow::Borrowed("cel")),
391 field_type: Some(crate::FieldType::Message),
392 key_type: None,
393 value_type: None,
394 subscript: Some(crate::Subscript::Index(cel_idx)),
395 },
396 ],
397 };
398 Err(v)
399 }
400 }
401 }
402
403 pub fn eval_map_values_cel(
409 &self,
410 this: Value,
411 field_path: FieldPath,
412 cel_idx: u64,
413 ) -> Result<(), Violation> {
414 let r = self.eval_value(this);
415 match r {
416 Ok(()) => Ok(()),
417 Err(mut v) => {
418 v.field = field_path;
419 v.rule = FieldPath {
420 elements: vec![
421 crate::FieldPathElement {
422 field_number: Some(19),
423 field_name: Some(Cow::Borrowed("map")),
424 field_type: Some(crate::FieldType::Message),
425 key_type: None,
426 value_type: None,
427 subscript: None,
428 },
429 crate::FieldPathElement {
430 field_number: Some(5),
431 field_name: Some(Cow::Borrowed("values")),
432 field_type: Some(crate::FieldType::Message),
433 key_type: None,
434 value_type: None,
435 subscript: None,
436 },
437 crate::FieldPathElement {
438 field_number: Some(23),
439 field_name: Some(Cow::Borrowed("cel")),
440 field_type: Some(crate::FieldType::Message),
441 key_type: None,
442 value_type: None,
443 subscript: Some(crate::Subscript::Index(cel_idx)),
444 },
445 ],
446 };
447 Err(v)
448 }
449 }
450 }
451
452 pub fn eval_predefined(
463 &self,
464 this: Value,
465 rule: Value,
466 field_path: FieldPath,
467 rule_path: FieldPath,
468 ) -> Result<(), Violation> {
469 let program = self.program.get_or_init(|| {
470 Program::compile(self.expression)
471 .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
472 });
473 let mut ctx = Context::default();
474 ctx.add_variable("this", this).expect("cel: 'this'");
475 ctx.add_variable("rule", rule).expect("cel: 'rule'");
476 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
477 .expect("cel: 'now'");
478 register_custom_functions(&mut ctx);
479 let result = program.execute(&ctx).map_err(|e| Violation {
480 field: field_path.clone(),
481 rule: rule_path.clone(),
482 rule_id: Cow::Borrowed(self.id),
483 message: Cow::Owned(format!("cel runtime error: {e}")),
484 for_key: false,
485 })?;
486 let ok = match result {
487 Value::Bool(true) => true,
488 Value::String(s) if s.is_empty() => true,
489 _ => false,
490 };
491 if ok {
492 return Ok(());
493 }
494 Err(Violation {
495 field: field_path,
496 rule: rule_path,
497 rule_id: Cow::Borrowed(self.id),
498 message: Cow::Borrowed(self.message),
499 for_key: false,
500 })
501 }
502
503 pub fn eval_value(&self, this: Value) -> Result<(), Violation> {
513 let program = self.program.get_or_init(|| {
514 Program::compile(self.expression)
515 .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
516 });
517 let mut ctx = Context::default();
518 ctx.add_variable("this", this).expect("cel: 'this' binding");
519 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
520 .expect("cel: 'now' binding");
521 register_custom_functions(&mut ctx);
522 let result = program
523 .execute(&ctx)
524 .map_err(|e| self.violation(Cow::Owned(format!("cel runtime error: {e}"))))?;
525 match result {
526 Value::Bool(true) => Ok(()),
527 Value::String(s) if s.is_empty() => Ok(()),
528 Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
529 Value::String(s) => {
530 if self.message.is_empty() {
531 Err(self.violation(Cow::Owned(s.to_string())))
532 } else {
533 Err(self.violation(Cow::Borrowed(self.message)))
534 }
535 }
536 other => Err(self.violation(Cow::Owned(format!(
537 "cel returned non-bool/string: {other:?}"
538 )))),
539 }
540 }
541
542 pub fn eval<T: AsCelValue>(&self, this: &T) -> Result<(), Violation> {
553 use cel_interpreter::ExecutionError;
554 let program = self.program.get_or_init(|| {
555 Program::compile(self.expression)
556 .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
557 });
558
559 let mut ctx = Context::default();
560 ctx.add_variable("this", this.as_cel_value())
561 .expect("cel: 'this' binding");
562 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
563 .expect("cel: 'now' binding");
564 register_custom_functions(&mut ctx);
565
566 let result = match program.execute(&ctx) {
572 Ok(v) => v,
573 Err(ExecutionError::NoSuchKey(_)) => return Ok(()),
574 Err(e @ ExecutionError::UnexpectedType { .. }) => {
575 return Err(Violation {
578 field: FieldPath::default(),
579 rule: FieldPath::default(),
580 rule_id: Cow::Borrowed("__cel_runtime_error__"),
581 message: Cow::Owned(e.to_string()),
582 for_key: false,
583 });
584 }
585 Err(e) => return Err(self.violation(Cow::Owned(format!("cel runtime error: {e}")))),
586 };
587
588 match result {
589 Value::Bool(true) => Ok(()),
590 Value::String(s) if s.is_empty() => Ok(()),
591 Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
592 Value::String(s) => {
593 if self.message.is_empty() {
594 Err(self.violation(Cow::Owned(s.to_string())))
595 } else {
596 Err(self.violation(Cow::Borrowed(self.message)))
597 }
598 }
599 other => Err(self.violation(Cow::Owned(format!(
600 "cel returned non-bool/string: {other:?}"
601 )))),
602 }
603 }
604
605 fn violation(&self, message: Cow<'static, str>) -> Violation {
606 Violation {
607 field: FieldPath::default(),
608 rule: FieldPath::default(),
609 rule_id: Cow::Borrowed(self.id),
610 message,
611 for_key: false,
612 }
613 }
614}
615
616#[expect(
617 clippy::too_many_lines,
618 reason = "one registration per CEL function — splitting scatters related registrations"
619)]
620fn register_custom_functions(ctx: &mut Context<'_>) {
621 const fn arg_i64(v: Option<&Value>) -> Option<i64> {
624 match v {
625 Some(Value::Int(n)) => Some(*n),
626 #[expect(
627 clippy::cast_possible_wrap,
628 reason = "CEL coerces u64 → i64 per spec; wrap is intended"
629 )]
630 Some(Value::UInt(n)) => Some(*n as i64),
631 _ => None,
632 }
633 }
634 const fn arg_bool(v: Option<&Value>) -> Option<bool> {
635 if let Some(Value::Bool(b)) = v {
636 Some(*b)
637 } else {
638 None
639 }
640 }
641
642 ctx.add_function("dyn", |This(v): This<i64>| -> i64 { v });
648
649 ctx.add_function("int", |This(v): This<Value>| -> i64 {
652 match v {
653 Value::Timestamp(t) => t.timestamp(),
654 Value::Int(i) => i,
655 #[expect(clippy::cast_possible_wrap, reason = "CEL int() on u64 wraps per spec")]
656 Value::UInt(u) => u as i64,
657 #[expect(
658 clippy::cast_possible_truncation,
659 reason = "CEL int() truncates float per spec"
660 )]
661 Value::Float(f) => f as i64,
662 Value::String(s) => s.parse::<i64>().unwrap_or(0),
663 Value::Bool(b) => i64::from(b),
664 _ => 0,
665 }
666 });
667 ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
674 crate::rules::string::is_uuid(&this)
675 });
676 ctx.add_function("isHostname", |This(this): This<Arc<String>>| -> bool {
677 crate::rules::string::is_hostname(&this)
678 });
679 ctx.add_function(
680 "isHostAndPort",
681 |This(this): This<Arc<String>>, port_required: bool| -> bool {
682 if crate::rules::string::is_host_and_port(&this) {
683 return true;
684 }
685 if port_required {
686 return false;
687 }
688 if crate::rules::string::is_hostname(&this)
690 || crate::rules::string::is_ipv4(&this)
691 || crate::rules::string::is_ipv6(&this)
692 {
693 return true;
694 }
695 if let Some(inner) = this.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
697 return crate::rules::string::is_ipv6(inner);
698 }
699 false
700 },
701 );
702 ctx.add_function("isEmail", |This(this): This<Arc<String>>| -> bool {
703 crate::rules::string::is_email(&this)
704 });
705 ctx.add_function("isUri", |This(this): This<Arc<String>>| -> bool {
706 crate::rules::string::is_uri(&this)
707 });
708 ctx.add_function("isUriRef", |This(this): This<Arc<String>>| -> bool {
709 crate::rules::string::is_uri_ref(&this)
710 });
711 ctx.add_function(
714 "isIp",
715 |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
716 let ver = arg_i64(args.first()).unwrap_or(0);
717 match ver {
718 0 => crate::rules::string::is_ip(&this),
719 4 => crate::rules::string::is_ipv4(&this),
720 6 => crate::rules::string::is_ipv6(&this),
721 _ => false,
722 }
723 },
724 );
725 ctx.add_function(
726 "isIpPrefix",
727 |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
728 let (ver, strict) = {
730 let a0 = args.first();
731 let a1 = args.get(1);
732 let v_i = arg_i64(a0);
733 let v_b = arg_bool(a0);
734 let i_i = arg_i64(a1);
735 let i_b = arg_bool(a1);
736 if let (Some(n), Some(b)) = (v_i, i_b) {
738 (n, Some(b))
739 } else if let Some(n) = v_i {
740 (n, i_b)
741 } else if let Some(b) = v_b {
742 (i_i.unwrap_or(0), Some(b))
743 } else {
744 (0, i_b)
745 }
746 };
747 let strict = strict.unwrap_or(false);
748 let addr_ok = match ver {
749 0 => true,
750 4 => {
751 this.parse::<::std::net::Ipv4Addr>().is_ok()
752 || crate::rules::string::is_ipv4_with_prefixlen(&this)
753 }
754 6 => {
755 this.parse::<::std::net::Ipv6Addr>().is_ok()
756 || crate::rules::string::is_ipv6_with_prefixlen(&this)
757 }
758 _ => return false,
759 };
760 if !addr_ok {
761 return false;
762 }
763 if strict {
764 match ver {
765 4 => crate::rules::string::is_ipv4_prefix(&this),
766 6 => crate::rules::string::is_ipv6_prefix(&this),
767 _ => crate::rules::string::is_ip_prefix(&this),
768 }
769 } else {
770 match ver {
771 4 => crate::rules::string::is_ipv4_with_prefixlen(&this),
772 6 => crate::rules::string::is_ipv6_with_prefixlen(&this),
773 _ => crate::rules::string::is_ip_with_prefixlen(&this),
774 }
775 }
776 },
777 );
778}