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<T: AsCelValue> ToCelValue for Option<T> {
91 fn to_cel_value(&self) -> Value {
92 self.as_ref().map_or(Value::Null, AsCelValue::as_cel_value)
93 }
94}
95
96impl<T: ToCelValue> ToCelValue for Vec<T> {
97 fn to_cel_value(&self) -> Value {
98 Value::List(
99 self.iter()
100 .map(ToCelValue::to_cel_value)
101 .collect::<Vec<_>>()
102 .into(),
103 )
104 }
105}
106
107impl<T: AsCelValue + Default> ToCelValue for buffa::MessageField<T> {
108 fn to_cel_value(&self) -> Value {
109 self.as_option()
110 .map_or(Value::Null, AsCelValue::as_cel_value)
111 }
112}
113
114impl AsCelValue for buffa_types::google::protobuf::FieldMask {
129 fn as_cel_value(&self) -> Value {
130 let paths: Vec<Value> = self
131 .paths
132 .iter()
133 .map(|p| Value::String(Arc::new(p.clone())))
134 .collect();
135 let mut map: std::collections::HashMap<crate::cel_core::objects::Key, Value> =
136 std::collections::HashMap::with_capacity(1);
137 map.insert(
138 crate::cel_core::objects::Key::String(Arc::new("paths".to_string())),
139 Value::List(Arc::new(paths)),
140 );
141 Value::Map(map.into())
142 }
143}
144
145impl AsCelValue for buffa_types::google::protobuf::Timestamp {
146 fn as_cel_value(&self) -> Value {
147 Value::Timestamp(timestamp_from_secs_nanos(self.seconds, self.nanos))
148 }
149}
150
151impl AsCelValue for buffa_types::google::protobuf::Duration {
152 fn as_cel_value(&self) -> Value {
153 Value::Duration(duration_from_secs_nanos(self.seconds, self.nanos))
154 }
155}
156
157impl<E: buffa::Enumeration> ToCelValue for buffa::EnumValue<E> {
158 fn to_cel_value(&self) -> Value {
159 Value::Int(i64::from(self.to_i32()))
160 }
161}
162
163macro_rules! impl_to_cel_for_hashmap_key {
164 ($kty:ty => $ktarget:ty) => {
165 impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
166 where
167 V: ToCelValue,
168 S: std::hash::BuildHasher,
169 {
170 fn to_cel_value(&self) -> Value {
171 let map: crate::cel_core::objects::Map = self
172 .iter()
173 .map(|(k, v)| {
174 (
175 crate::cel_core::objects::Key::from(k.clone() as $ktarget),
176 v.to_cel_value(),
177 )
178 })
179 .collect::<std::collections::HashMap<_, _>>()
180 .into();
181 Value::Map(map)
182 }
183 }
184 };
185 (string: $kty:ty) => {
186 impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
187 where
188 V: ToCelValue,
189 S: std::hash::BuildHasher,
190 {
191 fn to_cel_value(&self) -> Value {
192 let map: crate::cel_core::objects::Map = self
193 .iter()
194 .map(|(k, v)| {
195 (
196 crate::cel_core::objects::Key::from(k.clone()),
197 v.to_cel_value(),
198 )
199 })
200 .collect::<std::collections::HashMap<_, _>>()
201 .into();
202 Value::Map(map)
203 }
204 }
205 };
206}
207impl_to_cel_for_hashmap_key!(i32 => i64);
208impl_to_cel_for_hashmap_key!(u32 => u64);
209impl_to_cel_for_hashmap_key!(i64 => i64);
210impl_to_cel_for_hashmap_key!(u64 => u64);
211impl_to_cel_for_hashmap_key!(string: String);
212
213impl<V, S> ToCelValue for std::collections::HashMap<bool, V, S>
214where
215 V: ToCelValue,
216 S: std::hash::BuildHasher,
217{
218 fn to_cel_value(&self) -> Value {
219 let map: crate::cel_core::objects::Map = self
220 .iter()
221 .map(|(k, v)| (crate::cel_core::objects::Key::from(*k), v.to_cel_value()))
222 .collect::<std::collections::HashMap<_, _>>()
223 .into();
224 Value::Map(map)
225 }
226}
227
228pub fn enum_to_i32<E: buffa::Enumeration + Copy>(v: &E) -> i32 {
233 v.to_i32()
234}
235
236#[must_use]
237pub fn duration_from_secs_nanos(seconds: i64, nanos: i32) -> chrono::Duration {
238 chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(i64::from(nanos))
239}
240
241#[must_use]
246pub fn timestamp_from_secs_nanos(
247 seconds: i64,
248 nanos: i32,
249) -> chrono::DateTime<chrono::FixedOffset> {
250 let nanos_u32 = u32::try_from(nanos.max(0)).unwrap_or(0);
251 let s = chrono::DateTime::<chrono::Utc>::from_timestamp(seconds, nanos_u32)
252 .unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
253 s.fixed_offset()
254}
255
256pub fn to_cel_value<T: ToCelValue + ?Sized>(v: &T) -> Value {
257 v.to_cel_value()
258}
259
260impl CelConstraint {
261 #[must_use]
262 pub const fn new(id: &'static str, message: &'static str, expression: &'static str) -> Self {
263 Self {
264 id,
265 message,
266 expression,
267 program: OnceLock::new(),
268 }
269 }
270
271 pub fn eval_value_at(
289 &self,
290 this: Value,
291 field_path: FieldPath,
292 cel_index: u64,
293 ) -> Result<(), Violation> {
294 let r = self.eval_value(this);
295 match r {
296 Ok(()) => Ok(()),
297 Err(mut v) => {
298 v.field = field_path;
299 v.rule = FieldPath {
300 elements: vec![crate::FieldPathElement {
301 field_number: Some(23),
302 field_name: Some(Cow::Borrowed("cel")),
303 field_type: Some(crate::FieldType::Message),
304 key_type: None,
305 value_type: None,
306 subscript: Some(crate::Subscript::Index(cel_index)),
307 }],
308 };
309 Err(v)
310 }
311 }
312 }
313
314 pub fn eval_expr_value_at(
322 &self,
323 this: Value,
324 field_path: FieldPath,
325 index: u64,
326 ) -> Result<(), Violation> {
327 let r = self.eval_value(this);
328 match r {
329 Ok(()) => Ok(()),
330 Err(mut v) => {
331 v.field = field_path;
332 v.rule = FieldPath {
333 elements: vec![crate::FieldPathElement {
334 field_number: Some(29),
335 field_name: Some(Cow::Borrowed("cel_expression")),
336 field_type: Some(crate::FieldType::String),
337 key_type: None,
338 value_type: None,
339 subscript: Some(crate::Subscript::Index(index)),
340 }],
341 };
342 Err(v)
343 }
344 }
345 }
346
347 pub fn eval_repeated_items_cel(
354 &self,
355 this: Value,
356 field_path: FieldPath,
357 cel_idx: u64,
358 ) -> Result<(), Violation> {
359 let r = self.eval_value(this);
360 match r {
361 Ok(()) => Ok(()),
362 Err(mut v) => {
363 v.field = field_path;
364 v.rule = FieldPath {
365 elements: vec![
366 crate::FieldPathElement {
367 field_number: Some(18),
368 field_name: Some(Cow::Borrowed("repeated")),
369 field_type: Some(crate::FieldType::Message),
370 key_type: None,
371 value_type: None,
372 subscript: None,
373 },
374 crate::FieldPathElement {
375 field_number: Some(4),
376 field_name: Some(Cow::Borrowed("items")),
377 field_type: Some(crate::FieldType::Message),
378 key_type: None,
379 value_type: None,
380 subscript: None,
381 },
382 crate::FieldPathElement {
383 field_number: Some(23),
384 field_name: Some(Cow::Borrowed("cel")),
385 field_type: Some(crate::FieldType::Message),
386 key_type: None,
387 value_type: None,
388 subscript: Some(crate::Subscript::Index(cel_idx)),
389 },
390 ],
391 };
392 Err(v)
393 }
394 }
395 }
396
397 pub fn eval_map_keys_cel(
403 &self,
404 this: Value,
405 field_path: FieldPath,
406 cel_idx: u64,
407 ) -> Result<(), Violation> {
408 let r = self.eval_value(this);
409 match r {
410 Ok(()) => Ok(()),
411 Err(mut v) => {
412 v.field = field_path;
413 v.for_key = true;
414 v.rule = FieldPath {
415 elements: vec![
416 crate::FieldPathElement {
417 field_number: Some(19),
418 field_name: Some(Cow::Borrowed("map")),
419 field_type: Some(crate::FieldType::Message),
420 key_type: None,
421 value_type: None,
422 subscript: None,
423 },
424 crate::FieldPathElement {
425 field_number: Some(4),
426 field_name: Some(Cow::Borrowed("keys")),
427 field_type: Some(crate::FieldType::Message),
428 key_type: None,
429 value_type: None,
430 subscript: None,
431 },
432 crate::FieldPathElement {
433 field_number: Some(23),
434 field_name: Some(Cow::Borrowed("cel")),
435 field_type: Some(crate::FieldType::Message),
436 key_type: None,
437 value_type: None,
438 subscript: Some(crate::Subscript::Index(cel_idx)),
439 },
440 ],
441 };
442 Err(v)
443 }
444 }
445 }
446
447 pub fn eval_map_values_cel(
453 &self,
454 this: Value,
455 field_path: FieldPath,
456 cel_idx: u64,
457 ) -> Result<(), Violation> {
458 let r = self.eval_value(this);
459 match r {
460 Ok(()) => Ok(()),
461 Err(mut v) => {
462 v.field = field_path;
463 v.rule = FieldPath {
464 elements: vec![
465 crate::FieldPathElement {
466 field_number: Some(19),
467 field_name: Some(Cow::Borrowed("map")),
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(5),
475 field_name: Some(Cow::Borrowed("values")),
476 field_type: Some(crate::FieldType::Message),
477 key_type: None,
478 value_type: None,
479 subscript: None,
480 },
481 crate::FieldPathElement {
482 field_number: Some(23),
483 field_name: Some(Cow::Borrowed("cel")),
484 field_type: Some(crate::FieldType::Message),
485 key_type: None,
486 value_type: None,
487 subscript: Some(crate::Subscript::Index(cel_idx)),
488 },
489 ],
490 };
491 Err(v)
492 }
493 }
494 }
495
496 pub fn eval_predefined(
507 &self,
508 this: Value,
509 rule: Value,
510 field_path: FieldPath,
511 rule_path: FieldPath,
512 ) -> 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'");
519 ctx.add_variable("rule", rule).expect("cel: 'rule'");
520 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
521 .expect("cel: 'now'");
522 register_custom_functions(&mut ctx);
523 let result = program.execute(&ctx).map_err(|e| Violation {
524 field: field_path.clone(),
525 rule: rule_path.clone(),
526 rule_id: Cow::Borrowed(self.id),
527 message: Cow::Owned(format!("cel runtime error: {e}")),
528 for_key: false,
529 })?;
530 let ok = match result {
531 Value::Bool(true) => true,
532 Value::String(s) if s.is_empty() => true,
533 _ => false,
534 };
535 if ok {
536 return Ok(());
537 }
538 Err(Violation {
539 field: field_path,
540 rule: rule_path,
541 rule_id: Cow::Borrowed(self.id),
542 message: Cow::Borrowed(self.message),
543 for_key: false,
544 })
545 }
546
547 pub fn eval_value(&self, this: Value) -> Result<(), Violation> {
557 let program = self.program.get_or_init(|| {
558 Program::compile(self.expression)
559 .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
560 });
561 let mut ctx = Context::default();
562 ctx.add_variable("this", this).expect("cel: 'this' binding");
563 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
564 .expect("cel: 'now' binding");
565 register_custom_functions(&mut ctx);
566 let result = program
567 .execute(&ctx)
568 .map_err(|e| self.violation(Cow::Owned(format!("cel runtime error: {e}"))))?;
569 match result {
570 Value::Bool(true) => Ok(()),
571 Value::String(s) if s.is_empty() => Ok(()),
572 Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
573 Value::String(s) => {
574 if self.message.is_empty() {
575 Err(self.violation(Cow::Owned(s.to_string())))
576 } else {
577 Err(self.violation(Cow::Borrowed(self.message)))
578 }
579 }
580 other => Err(self.violation(Cow::Owned(format!(
581 "cel returned non-bool/string: {other:?}"
582 )))),
583 }
584 }
585
586 pub fn eval<T: AsCelValue>(&self, this: &T) -> Result<(), Violation> {
597 use crate::cel_core::ExecutionError;
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
603 let mut ctx = Context::default();
604 ctx.add_variable("this", this.as_cel_value())
605 .expect("cel: 'this' binding");
606 ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
607 .expect("cel: 'now' binding");
608 register_custom_functions(&mut ctx);
609
610 let result = match program.execute(&ctx) {
616 Ok(v) => v,
617 Err(ExecutionError::NoSuchKey(_)) => return Ok(()),
618 Err(e @ ExecutionError::UnexpectedType { .. }) => {
619 return Err(Violation {
622 field: FieldPath::default(),
623 rule: FieldPath::default(),
624 rule_id: Cow::Borrowed("__cel_runtime_error__"),
625 message: Cow::Owned(e.to_string()),
626 for_key: false,
627 });
628 }
629 Err(e) => return Err(self.violation(Cow::Owned(format!("cel runtime error: {e}")))),
630 };
631
632 match result {
633 Value::Bool(true) => Ok(()),
634 Value::String(s) if s.is_empty() => Ok(()),
635 Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
636 Value::String(s) => {
637 if self.message.is_empty() {
638 Err(self.violation(Cow::Owned(s.to_string())))
639 } else {
640 Err(self.violation(Cow::Borrowed(self.message)))
641 }
642 }
643 other => Err(self.violation(Cow::Owned(format!(
644 "cel returned non-bool/string: {other:?}"
645 )))),
646 }
647 }
648
649 fn violation(&self, message: Cow<'static, str>) -> Violation {
650 Violation {
651 field: FieldPath::default(),
652 rule: FieldPath::default(),
653 rule_id: Cow::Borrowed(self.id),
654 message,
655 for_key: false,
656 }
657 }
658}
659
660#[expect(
661 clippy::too_many_lines,
662 reason = "one registration per CEL function — splitting scatters related registrations"
663)]
664fn register_custom_functions(ctx: &mut Context<'_>) {
665 const fn arg_i64(v: Option<&Value>) -> Option<i64> {
668 match v {
669 Some(Value::Int(n)) => Some(*n),
670 #[expect(
671 clippy::cast_possible_wrap,
672 reason = "CEL coerces u64 → i64 per spec; wrap is intended"
673 )]
674 Some(Value::UInt(n)) => Some(*n as i64),
675 _ => None,
676 }
677 }
678 const fn arg_bool(v: Option<&Value>) -> Option<bool> {
679 if let Some(Value::Bool(b)) = v {
680 Some(*b)
681 } else {
682 None
683 }
684 }
685
686 ctx.add_function("dyn", |This(v): This<i64>| -> i64 { v });
692
693 ctx.add_function("int", |This(v): This<Value>| -> i64 {
696 match v {
697 Value::Timestamp(t) => t.timestamp(),
698 Value::Int(i) => i,
699 #[expect(clippy::cast_possible_wrap, reason = "CEL int() on u64 wraps per spec")]
700 Value::UInt(u) => u as i64,
701 #[expect(
702 clippy::cast_possible_truncation,
703 reason = "CEL int() truncates float per spec"
704 )]
705 Value::Float(f) => f as i64,
706 Value::String(s) => s.parse::<i64>().unwrap_or(0),
707 Value::Bool(b) => i64::from(b),
708 _ => 0,
709 }
710 });
711 ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
718 crate::rules::string::is_uuid(&this)
719 });
720 ctx.add_function("isHostname", |This(this): This<Arc<String>>| -> bool {
721 crate::rules::string::is_hostname(&this)
722 });
723 ctx.add_function(
724 "isHostAndPort",
725 |This(this): This<Arc<String>>, port_required: bool| -> bool {
726 if crate::rules::string::is_host_and_port(&this) {
727 return true;
728 }
729 if port_required {
730 return false;
731 }
732 if crate::rules::string::is_hostname(&this)
734 || crate::rules::string::is_ipv4(&this)
735 || crate::rules::string::is_ipv6(&this)
736 {
737 return true;
738 }
739 if let Some(inner) = this.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
741 return crate::rules::string::is_ipv6(inner);
742 }
743 false
744 },
745 );
746 ctx.add_function("isEmail", |This(this): This<Arc<String>>| -> bool {
747 crate::rules::string::is_email(&this)
748 });
749 ctx.add_function("isUri", |This(this): This<Arc<String>>| -> bool {
750 crate::rules::string::is_uri(&this)
751 });
752 ctx.add_function("isUriRef", |This(this): This<Arc<String>>| -> bool {
753 crate::rules::string::is_uri_ref(&this)
754 });
755 ctx.add_function(
758 "isIp",
759 |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
760 let ver = arg_i64(args.first()).unwrap_or(0);
761 match ver {
762 0 => crate::rules::string::is_ip(&this),
763 4 => crate::rules::string::is_ipv4(&this),
764 6 => crate::rules::string::is_ipv6(&this),
765 _ => false,
766 }
767 },
768 );
769 ctx.add_function(
770 "isIpPrefix",
771 |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
772 let (ver, strict) = {
774 let a0 = args.first();
775 let a1 = args.get(1);
776 let v_i = arg_i64(a0);
777 let v_b = arg_bool(a0);
778 let i_i = arg_i64(a1);
779 let i_b = arg_bool(a1);
780 if let (Some(n), Some(b)) = (v_i, i_b) {
782 (n, Some(b))
783 } else if let Some(n) = v_i {
784 (n, i_b)
785 } else if let Some(b) = v_b {
786 (i_i.unwrap_or(0), Some(b))
787 } else {
788 (0, i_b)
789 }
790 };
791 let strict = strict.unwrap_or(false);
792 let addr_ok = match ver {
793 0 => true,
794 4 => {
795 this.parse::<::std::net::Ipv4Addr>().is_ok()
796 || crate::rules::string::is_ipv4_with_prefixlen(&this)
797 }
798 6 => {
799 this.parse::<::std::net::Ipv6Addr>().is_ok()
800 || crate::rules::string::is_ipv6_with_prefixlen(&this)
801 }
802 _ => return false,
803 };
804 if !addr_ok {
805 return false;
806 }
807 if strict {
808 match ver {
809 4 => crate::rules::string::is_ipv4_prefix(&this),
810 6 => crate::rules::string::is_ipv6_prefix(&this),
811 _ => crate::rules::string::is_ip_prefix(&this),
812 }
813 } else {
814 match ver {
815 4 => crate::rules::string::is_ipv4_with_prefixlen(&this),
816 6 => crate::rules::string::is_ipv6_with_prefixlen(&this),
817 _ => crate::rules::string::is_ip_with_prefixlen(&this),
818 }
819 }
820 },
821 );
822}