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