1use std::{
2 borrow::Cow,
3 sync::{Arc, OnceLock},
4};
5
6use crate::{
7 FieldPath, Violation,
8 cel_core::{
9 Context, Program, Value,
10 extractors::{Arguments, This},
11 },
12};
13
14pub struct CelConstraint {
15 pub id: &'static str,
16 pub message: &'static str,
17 pub expression: &'static str,
18 program: OnceLock<Program>,
19}
20
21pub trait AsCelValue {
22 fn as_cel_value(&self) -> Value;
23}
24
25pub trait ToCelValue {
27 fn to_cel_value(&self) -> Value;
28}
29
30impl ToCelValue for String {
31 fn to_cel_value(&self) -> Value {
32 Value::String(self.clone().into())
33 }
34}
35
36impl ToCelValue for str {
37 fn to_cel_value(&self) -> Value {
38 Value::String(self.to_string().into())
39 }
40}
41
42impl ToCelValue for i32 {
43 fn to_cel_value(&self) -> Value {
44 Value::Int(i64::from(*self))
45 }
46}
47
48impl ToCelValue for i64 {
49 fn to_cel_value(&self) -> Value {
50 Value::Int(*self)
51 }
52}
53
54impl ToCelValue for u32 {
55 fn to_cel_value(&self) -> Value {
56 Value::UInt(u64::from(*self))
57 }
58}
59
60impl ToCelValue for u64 {
61 fn to_cel_value(&self) -> Value {
62 Value::UInt(*self)
63 }
64}
65
66impl ToCelValue for f32 {
67 fn to_cel_value(&self) -> Value {
68 Value::Float(f64::from(*self))
69 }
70}
71
72impl ToCelValue for f64 {
73 fn to_cel_value(&self) -> Value {
74 Value::Float(*self)
75 }
76}
77
78impl ToCelValue for bool {
79 fn to_cel_value(&self) -> Value {
80 Value::Bool(*self)
81 }
82}
83
84impl ToCelValue for Vec<u8> {
85 fn to_cel_value(&self) -> Value {
86 Value::Bytes(self.clone().into())
87 }
88}
89
90impl ToCelValue for buffa::bytes::Bytes {
91 fn to_cel_value(&self) -> Value {
92 Value::Bytes(self.to_vec().into())
93 }
94}
95
96impl<T: AsCelValue> ToCelValue for Option<T> {
97 fn to_cel_value(&self) -> Value {
98 self.as_ref().map_or(Value::Null, AsCelValue::as_cel_value)
99 }
100}
101
102impl<T: ToCelValue> ToCelValue for Vec<T> {
103 fn to_cel_value(&self) -> Value {
104 Value::List(
105 self.iter()
106 .map(ToCelValue::to_cel_value)
107 .collect::<Vec<_>>()
108 .into(),
109 )
110 }
111}
112
113impl<T: AsCelValue + Default> ToCelValue for buffa::MessageField<T> {
114 fn to_cel_value(&self) -> Value {
115 self.as_option()
116 .map_or(Value::Null, AsCelValue::as_cel_value)
117 }
118}
119
120impl AsCelValue for buffa_types::google::protobuf::Any {
134 fn as_cel_value(&self) -> Value {
135 let mut map: std::collections::HashMap<String, Value> =
136 std::collections::HashMap::with_capacity(2);
137 map.insert("type_url".to_string(), self.type_url.to_cel_value());
138 map.insert("value".to_string(), self.value.to_cel_value());
139 Value::Map(map.into())
140 }
141}
142
143impl AsCelValue for buffa_types::google::protobuf::Empty {
144 fn as_cel_value(&self) -> Value {
145 Value::Map(std::collections::HashMap::<String, Value>::new().into())
146 }
147}
148
149impl AsCelValue for buffa_types::google::protobuf::FieldMask {
150 fn as_cel_value(&self) -> Value {
151 let paths: Vec<Value> = self
152 .paths
153 .iter()
154 .map(|p| Value::String(Arc::new(p.clone())))
155 .collect();
156 let mut map: std::collections::HashMap<crate::cel_core::objects::Key, Value> =
157 std::collections::HashMap::with_capacity(1);
158 map.insert(
159 crate::cel_core::objects::Key::String(Arc::new("paths".to_string())),
160 Value::List(Arc::new(paths)),
161 );
162 Value::Map(map.into())
163 }
164}
165
166impl AsCelValue for buffa_types::google::protobuf::Timestamp {
167 fn as_cel_value(&self) -> Value {
168 Value::Timestamp(timestamp_from_secs_nanos(self.seconds, self.nanos))
169 }
170}
171
172impl AsCelValue for buffa_types::google::protobuf::Duration {
173 fn as_cel_value(&self) -> Value {
174 Value::Duration(duration_from_secs_nanos(self.seconds, self.nanos))
175 }
176}
177
178macro_rules! impl_to_cel_for_as_cel_wkt {
179 ($($ty:path),* $(,)?) => {
180 $(
181 impl ToCelValue for $ty {
182 fn to_cel_value(&self) -> Value {
183 self.as_cel_value()
184 }
185 }
186 )*
187 };
188}
189
190impl_to_cel_for_as_cel_wkt!(
191 buffa_types::google::protobuf::Any,
192 buffa_types::google::protobuf::Empty,
193 buffa_types::google::protobuf::FieldMask,
194 buffa_types::google::protobuf::Timestamp,
195 buffa_types::google::protobuf::Duration,
196);
197
198impl<E: buffa::Enumeration> ToCelValue for buffa::EnumValue<E> {
199 fn to_cel_value(&self) -> Value {
200 Value::Int(i64::from(self.to_i32()))
201 }
202}
203
204macro_rules! impl_to_cel_for_hashmap_key {
205 ($kty:ty => $ktarget:ty) => {
206 impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
207 where
208 V: ToCelValue,
209 S: std::hash::BuildHasher,
210 {
211 fn to_cel_value(&self) -> Value {
212 let map: crate::cel_core::objects::Map = self
213 .iter()
214 .map(|(k, v)| {
215 (
216 crate::cel_core::objects::Key::from(k.clone() as $ktarget),
217 v.to_cel_value(),
218 )
219 })
220 .collect::<std::collections::HashMap<_, _>>()
221 .into();
222 Value::Map(map)
223 }
224 }
225 };
226 (string: $kty:ty) => {
227 impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
228 where
229 V: ToCelValue,
230 S: std::hash::BuildHasher,
231 {
232 fn to_cel_value(&self) -> Value {
233 let map: crate::cel_core::objects::Map = self
234 .iter()
235 .map(|(k, v)| {
236 (
237 crate::cel_core::objects::Key::from(k.clone()),
238 v.to_cel_value(),
239 )
240 })
241 .collect::<std::collections::HashMap<_, _>>()
242 .into();
243 Value::Map(map)
244 }
245 }
246 };
247}
248impl_to_cel_for_hashmap_key!(i32 => i64);
249impl_to_cel_for_hashmap_key!(u32 => u64);
250impl_to_cel_for_hashmap_key!(i64 => i64);
251impl_to_cel_for_hashmap_key!(u64 => u64);
252impl_to_cel_for_hashmap_key!(string: String);
253
254impl<V, S> ToCelValue for std::collections::HashMap<bool, V, S>
255where
256 V: ToCelValue,
257 S: std::hash::BuildHasher,
258{
259 fn to_cel_value(&self) -> Value {
260 let map: crate::cel_core::objects::Map = self
261 .iter()
262 .map(|(k, v)| (crate::cel_core::objects::Key::from(*k), v.to_cel_value()))
263 .collect::<std::collections::HashMap<_, _>>()
264 .into();
265 Value::Map(map)
266 }
267}
268
269pub fn enum_to_i32<E: buffa::Enumeration + Copy>(v: &E) -> i32 {
274 v.to_i32()
275}
276
277#[must_use]
278pub fn duration_from_secs_nanos(seconds: i64, nanos: i32) -> chrono::Duration {
279 chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(i64::from(nanos))
280}
281
282#[must_use]
287pub fn timestamp_from_secs_nanos(
288 seconds: i64,
289 nanos: i32,
290) -> chrono::DateTime<chrono::FixedOffset> {
291 let nanos_u32 = u32::try_from(nanos.max(0)).unwrap_or(0);
292 let s = chrono::DateTime::<chrono::Utc>::from_timestamp(seconds, nanos_u32)
293 .unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
294 s.fixed_offset()
295}
296
297pub fn to_cel_value<T: ToCelValue + ?Sized>(v: &T) -> Value {
298 v.to_cel_value()
299}
300
301impl CelConstraint {
302 #[must_use]
303 pub const fn new(id: &'static str, message: &'static str, expression: &'static str) -> Self {
304 Self {
305 id,
306 message,
307 expression,
308 program: OnceLock::new(),
309 }
310 }
311
312 pub fn eval_value_at(
330 &self,
331 this: Value,
332 field_path: FieldPath,
333 cel_index: u64,
334 ) -> Result<(), Violation> {
335 let r = self.eval_value(this);
336 match r {
337 Ok(()) => Ok(()),
338 Err(mut v) => {
339 v.field = field_path;
340 v.rule = FieldPath {
341 elements: vec![crate::FieldPathElement {
342 field_number: Some(23),
343 field_name: Some(Cow::Borrowed("cel")),
344 field_type: Some(crate::FieldType::Message),
345 key_type: None,
346 value_type: None,
347 subscript: Some(crate::Subscript::Index(cel_index)),
348 }],
349 };
350 Err(v)
351 }
352 }
353 }
354
355 pub fn eval_expr_value_at(
363 &self,
364 this: Value,
365 field_path: FieldPath,
366 index: u64,
367 ) -> Result<(), Violation> {
368 let r = self.eval_value(this);
369 match r {
370 Ok(()) => Ok(()),
371 Err(mut v) => {
372 v.field = field_path;
373 v.rule = FieldPath {
374 elements: vec![crate::FieldPathElement {
375 field_number: Some(29),
376 field_name: Some(Cow::Borrowed("cel_expression")),
377 field_type: Some(crate::FieldType::String),
378 key_type: None,
379 value_type: None,
380 subscript: Some(crate::Subscript::Index(index)),
381 }],
382 };
383 Err(v)
384 }
385 }
386 }
387
388 pub fn eval_repeated_items_cel(
395 &self,
396 this: Value,
397 field_path: FieldPath,
398 cel_idx: u64,
399 ) -> Result<(), Violation> {
400 let r = self.eval_value(this);
401 match r {
402 Ok(()) => Ok(()),
403 Err(mut v) => {
404 v.field = field_path;
405 v.rule = FieldPath {
406 elements: vec![
407 crate::FieldPathElement {
408 field_number: Some(18),
409 field_name: Some(Cow::Borrowed("repeated")),
410 field_type: Some(crate::FieldType::Message),
411 key_type: None,
412 value_type: None,
413 subscript: None,
414 },
415 crate::FieldPathElement {
416 field_number: Some(4),
417 field_name: Some(Cow::Borrowed("items")),
418 field_type: Some(crate::FieldType::Message),
419 key_type: None,
420 value_type: None,
421 subscript: None,
422 },
423 crate::FieldPathElement {
424 field_number: Some(23),
425 field_name: Some(Cow::Borrowed("cel")),
426 field_type: Some(crate::FieldType::Message),
427 key_type: None,
428 value_type: None,
429 subscript: Some(crate::Subscript::Index(cel_idx)),
430 },
431 ],
432 };
433 Err(v)
434 }
435 }
436 }
437
438 pub fn eval_map_keys_cel(
444 &self,
445 this: Value,
446 field_path: FieldPath,
447 cel_idx: u64,
448 ) -> Result<(), Violation> {
449 let r = self.eval_value(this);
450 match r {
451 Ok(()) => Ok(()),
452 Err(mut v) => {
453 v.field = field_path;
454 v.for_key = true;
455 v.rule = FieldPath {
456 elements: vec![
457 crate::FieldPathElement {
458 field_number: Some(19),
459 field_name: Some(Cow::Borrowed("map")),
460 field_type: Some(crate::FieldType::Message),
461 key_type: None,
462 value_type: None,
463 subscript: None,
464 },
465 crate::FieldPathElement {
466 field_number: Some(4),
467 field_name: Some(Cow::Borrowed("keys")),
468 field_type: Some(crate::FieldType::Message),
469 key_type: None,
470 value_type: None,
471 subscript: None,
472 },
473 crate::FieldPathElement {
474 field_number: Some(23),
475 field_name: Some(Cow::Borrowed("cel")),
476 field_type: Some(crate::FieldType::Message),
477 key_type: None,
478 value_type: None,
479 subscript: Some(crate::Subscript::Index(cel_idx)),
480 },
481 ],
482 };
483 Err(v)
484 }
485 }
486 }
487
488 pub fn eval_map_values_cel(
494 &self,
495 this: Value,
496 field_path: FieldPath,
497 cel_idx: u64,
498 ) -> Result<(), Violation> {
499 let r = self.eval_value(this);
500 match r {
501 Ok(()) => Ok(()),
502 Err(mut v) => {
503 v.field = field_path;
504 v.rule = FieldPath {
505 elements: vec![
506 crate::FieldPathElement {
507 field_number: Some(19),
508 field_name: Some(Cow::Borrowed("map")),
509 field_type: Some(crate::FieldType::Message),
510 key_type: None,
511 value_type: None,
512 subscript: None,
513 },
514 crate::FieldPathElement {
515 field_number: Some(5),
516 field_name: Some(Cow::Borrowed("values")),
517 field_type: Some(crate::FieldType::Message),
518 key_type: None,
519 value_type: None,
520 subscript: None,
521 },
522 crate::FieldPathElement {
523 field_number: Some(23),
524 field_name: Some(Cow::Borrowed("cel")),
525 field_type: Some(crate::FieldType::Message),
526 key_type: None,
527 value_type: None,
528 subscript: Some(crate::Subscript::Index(cel_idx)),
529 },
530 ],
531 };
532 Err(v)
533 }
534 }
535 }
536
537 pub fn eval_predefined(
548 &self,
549 this: Value,
550 rule: Value,
551 field_path: FieldPath,
552 rule_path: FieldPath,
553 ) -> Result<(), Violation> {
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 let mut ctx = Context::default();
559 ctx.add_variable("this", this).expect("cel: 'this'");
560 ctx.add_variable("rule", rule).expect("cel: 'rule'");
561 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
562 .expect("cel: 'now'");
563 register_custom_functions(&mut ctx);
564 let result = program.execute(&ctx).map_err(|e| Violation {
565 field: field_path.clone(),
566 rule: rule_path.clone(),
567 rule_id: Cow::Borrowed(self.id),
568 message: Cow::Owned(format!("cel runtime error: {e}")),
569 for_key: false,
570 })?;
571 let ok = match result {
572 Value::Bool(true) => true,
573 Value::String(s) if s.is_empty() => true,
574 _ => false,
575 };
576 if ok {
577 return Ok(());
578 }
579 Err(Violation {
580 field: field_path,
581 rule: rule_path,
582 rule_id: Cow::Borrowed(self.id),
583 message: Cow::Borrowed(self.message),
584 for_key: false,
585 })
586 }
587
588 pub fn eval_value(&self, this: Value) -> Result<(), Violation> {
598 let program = self.program.get_or_init(|| {
599 Program::compile(self.expression)
600 .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
601 });
602 let mut ctx = Context::default();
603 ctx.add_variable("this", this).expect("cel: 'this' binding");
604 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
605 .expect("cel: 'now' binding");
606 register_custom_functions(&mut ctx);
607 let result = program
608 .execute(&ctx)
609 .map_err(|e| self.violation(Cow::Owned(format!("cel runtime error: {e}"))))?;
610 match result {
611 Value::Bool(true) => Ok(()),
612 Value::String(s) if s.is_empty() => Ok(()),
613 Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
614 Value::String(s) => {
615 if self.message.is_empty() {
616 Err(self.violation(Cow::Owned(s.to_string())))
617 } else {
618 Err(self.violation(Cow::Borrowed(self.message)))
619 }
620 }
621 other => Err(self.violation(Cow::Owned(format!(
622 "cel returned non-bool/string: {other:?}"
623 )))),
624 }
625 }
626
627 pub fn eval<T: AsCelValue>(&self, this: &T) -> Result<(), Violation> {
638 use crate::cel_core::ExecutionError;
639 let program = self.program.get_or_init(|| {
640 Program::compile(self.expression)
641 .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
642 });
643
644 let mut ctx = Context::default();
645 ctx.add_variable("this", this.as_cel_value())
646 .expect("cel: 'this' binding");
647 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
648 .expect("cel: 'now' binding");
649 register_custom_functions(&mut ctx);
650
651 let result = match program.execute(&ctx) {
657 Ok(v) => v,
658 Err(ExecutionError::NoSuchKey(_)) => return Ok(()),
659 Err(e @ ExecutionError::UnexpectedType { .. }) => {
660 return Err(Violation {
663 field: FieldPath::default(),
664 rule: FieldPath::default(),
665 rule_id: Cow::Borrowed("__cel_runtime_error__"),
666 message: Cow::Owned(e.to_string()),
667 for_key: false,
668 });
669 }
670 Err(e) => return Err(self.violation(Cow::Owned(format!("cel runtime error: {e}")))),
671 };
672
673 match result {
674 Value::Bool(true) => Ok(()),
675 Value::String(s) if s.is_empty() => Ok(()),
676 Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
677 Value::String(s) => {
678 if self.message.is_empty() {
679 Err(self.violation(Cow::Owned(s.to_string())))
680 } else {
681 Err(self.violation(Cow::Borrowed(self.message)))
682 }
683 }
684 other => Err(self.violation(Cow::Owned(format!(
685 "cel returned non-bool/string: {other:?}"
686 )))),
687 }
688 }
689
690 fn violation(&self, message: Cow<'static, str>) -> Violation {
691 Violation {
692 field: FieldPath::default(),
693 rule: FieldPath::default(),
694 rule_id: Cow::Borrowed(self.id),
695 message,
696 for_key: false,
697 }
698 }
699}
700
701#[expect(
702 clippy::too_many_lines,
703 reason = "one registration per CEL function — splitting scatters related registrations"
704)]
705fn register_custom_functions(ctx: &mut Context<'_>) {
706 const fn arg_i64(v: Option<&Value>) -> Option<i64> {
709 match v {
710 Some(Value::Int(n)) => Some(*n),
711 #[expect(
712 clippy::cast_possible_wrap,
713 reason = "CEL coerces u64 → i64 per spec; wrap is intended"
714 )]
715 Some(Value::UInt(n)) => Some(*n as i64),
716 _ => None,
717 }
718 }
719 const fn arg_bool(v: Option<&Value>) -> Option<bool> {
720 if let Some(Value::Bool(b)) = v {
721 Some(*b)
722 } else {
723 None
724 }
725 }
726
727 ctx.add_function("dyn", |This(v): This<i64>| -> i64 { v });
733
734 ctx.add_function("int", |This(v): This<Value>| -> i64 {
737 match v {
738 Value::Timestamp(t) => t.timestamp(),
739 Value::Int(i) => i,
740 #[expect(clippy::cast_possible_wrap, reason = "CEL int() on u64 wraps per spec")]
741 Value::UInt(u) => u as i64,
742 #[expect(
743 clippy::cast_possible_truncation,
744 reason = "CEL int() truncates float per spec"
745 )]
746 Value::Float(f) => f as i64,
747 Value::String(s) => s.parse::<i64>().unwrap_or(0),
748 Value::Bool(b) => i64::from(b),
749 _ => 0,
750 }
751 });
752 ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
759 crate::rules::string::is_uuid(&this)
760 });
761 ctx.add_function("isHostname", |This(this): This<Arc<String>>| -> bool {
762 crate::rules::string::is_hostname(&this)
763 });
764 ctx.add_function(
765 "isHostAndPort",
766 |This(this): This<Arc<String>>, port_required: bool| -> bool {
767 if crate::rules::string::is_host_and_port(&this) {
768 return true;
769 }
770 if port_required {
771 return false;
772 }
773 if crate::rules::string::is_hostname(&this)
775 || crate::rules::string::is_ipv4(&this)
776 || crate::rules::string::is_ipv6(&this)
777 {
778 return true;
779 }
780 if let Some(inner) = this.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
782 return crate::rules::string::is_ipv6(inner);
783 }
784 false
785 },
786 );
787 ctx.add_function("isEmail", |This(this): This<Arc<String>>| -> bool {
788 crate::rules::string::is_email(&this)
789 });
790 ctx.add_function("isUri", |This(this): This<Arc<String>>| -> bool {
791 crate::rules::string::is_uri(&this)
792 });
793 ctx.add_function("isUriRef", |This(this): This<Arc<String>>| -> bool {
794 crate::rules::string::is_uri_ref(&this)
795 });
796 ctx.add_function(
799 "isIp",
800 |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
801 let ver = arg_i64(args.first()).unwrap_or(0);
802 match ver {
803 0 => crate::rules::string::is_ip(&this),
804 4 => crate::rules::string::is_ipv4(&this),
805 6 => crate::rules::string::is_ipv6(&this),
806 _ => false,
807 }
808 },
809 );
810 ctx.add_function(
811 "isIpPrefix",
812 |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
813 let (ver, strict) = {
815 let a0 = args.first();
816 let a1 = args.get(1);
817 let v_i = arg_i64(a0);
818 let v_b = arg_bool(a0);
819 let i_i = arg_i64(a1);
820 let i_b = arg_bool(a1);
821 if let (Some(n), Some(b)) = (v_i, i_b) {
823 (n, Some(b))
824 } else if let Some(n) = v_i {
825 (n, i_b)
826 } else if let Some(b) = v_b {
827 (i_i.unwrap_or(0), Some(b))
828 } else {
829 (0, i_b)
830 }
831 };
832 let strict = strict.unwrap_or(false);
833 let addr_ok = match ver {
834 0 => true,
835 4 => {
836 this.parse::<::std::net::Ipv4Addr>().is_ok()
837 || crate::rules::string::is_ipv4_with_prefixlen(&this)
838 }
839 6 => {
840 this.parse::<::std::net::Ipv6Addr>().is_ok()
841 || crate::rules::string::is_ipv6_with_prefixlen(&this)
842 }
843 _ => return false,
844 };
845 if !addr_ok {
846 return false;
847 }
848 if strict {
849 match ver {
850 4 => crate::rules::string::is_ipv4_prefix(&this),
851 6 => crate::rules::string::is_ipv6_prefix(&this),
852 _ => crate::rules::string::is_ip_prefix(&this),
853 }
854 } else {
855 match ver {
856 4 => crate::rules::string::is_ipv4_with_prefixlen(&this),
857 6 => crate::rules::string::is_ipv6_with_prefixlen(&this),
858 _ => crate::rules::string::is_ip_with_prefixlen(&this),
859 }
860 }
861 },
862 );
863}