Skip to main content

protovalidate_buffa/
cel.rs

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
25/// Proto scalar / list → CEL value conversion, used by plugin-emitted `AsCelValue` impls.
26pub 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
120// ---------------------------------------------------------------------------
121// Well-Known Type `AsCelValue` impls.
122//
123// These let plugin-emitted code call
124// `::protovalidate_buffa::cel::AsCelValue::as_cel_value(inner)` for fields of
125// type `google.protobuf.FieldMask`, `google.protobuf.Timestamp`, and
126// `google.protobuf.Duration` — including the predefined-rule path, which binds
127// `this` to the field value via `AsCelValue`.
128//
129// `Any`, `Empty`, and `FieldMask` are exposed as CEL maps. `Timestamp` and
130// `Duration` are exposed as native CEL `Timestamp` / `Duration` values, which
131// carry their own comparison and arithmetic operators in CEL.
132
133impl 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
269/// Generic adapter used by emitted impls: accepts anything that implements `ToCelValue`.
270///
271/// Convert any enum-like value to i32. Works for buffa `EnumValue<E>` and raw
272/// enum types (which can be cast via `Enumeration::to_i32`).
273pub 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/// # Panics
283///
284/// Panics only if the fallback `from_timestamp(0, 0)` fails, which is
285/// impossible (Unix epoch is always representable).
286#[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    /// Evaluates this CEL expression against `this` (bound as the `this` variable
313    /// inside the expression) plus a per-call-frozen `now` timestamp.
314    ///
315    /// # Errors
316    ///
317    /// Returns a [`Violation`] when the compiled CEL expression returns
318    /// `false`, returns a non-empty string, or produces a runtime error.
319    ///
320    /// # Panics
321    ///
322    /// Panics at first call if the CEL expression fails to compile (it is
323    /// baked in at codegen time, so a parse failure indicates a plugin bug).
324    /// Evaluate this CEL expression with `this` bound to the supplied raw
325    /// `crate::cel_core::Value` (used for scalar-field-level CEL rules where
326    /// the "this" target is a primitive, not a message). The violation's
327    /// `field` path is set from `field_path` and `rule` path is set to
328    /// `[cel[index]]` reflecting position in the repeated `FieldRules.cel`.
329    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    /// Like `eval_value_at` but for `(field).cel_expression` (string)
356    /// constraints — the rule path uses `field_number=29`, `field_name="cel_expression"`,
357    /// `field_type=String` instead of cel's `field_number=23`/`field_type=Message`.
358    ///
359    /// # Errors
360    ///
361    /// Returns a [`Violation`] when the CEL expression rejects `this`.
362    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    /// Evaluate for a `repeated.items.cel[idx]` rule. Rule path is
389    /// `[repeated(18), items(4), cel(23, index:cel_idx)]`.
390    ///
391    /// # Errors
392    ///
393    /// Returns a [`Violation`] when the CEL expression rejects `this`.
394    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    /// `map.keys.cel[idx]` — `for_key=true`.
439    ///
440    /// # Errors
441    ///
442    /// Returns a [`Violation`] when the CEL expression rejects `this`.
443    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    /// `map.values.cel[idx]`.
489    ///
490    /// # Errors
491    ///
492    /// Returns a [`Violation`] when the CEL expression rejects `this`.
493    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    /// Evaluate a predefined-rule CEL expression with `this` and `rule`
538    /// bindings. Caller supplies the complete `field_path` and `rule_path`.
539    ///
540    /// # Errors
541    ///
542    /// Returns a [`Violation`] when the CEL expression rejects `this`.
543    ///
544    /// # Panics
545    ///
546    /// Panics if the CEL expression fails to compile (baked in at codegen time).
547    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    /// Evaluate with `this` already bound as a raw CEL [`Value`].
589    ///
590    /// # Errors
591    ///
592    /// Returns a [`Violation`] when the CEL expression rejects `this`.
593    ///
594    /// # Panics
595    ///
596    /// Panics if the CEL expression fails to compile (baked in at codegen time).
597    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    /// Evaluate with `this` bound via the [`AsCelValue`] trait.
628    ///
629    /// # Errors
630    ///
631    /// Returns a [`Violation`] when the CEL expression rejects `this` or when
632    /// a non-skippable runtime error occurs.
633    ///
634    /// # Panics
635    ///
636    /// Panics if the CEL expression fails to compile (baked in at codegen time).
637    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        // Execute. `NoSuchKey` on message-level eval is not a hard error —
652        // protovalidate semantics treat missing submessages as zero-valued,
653        // so we skip the rule. `UnexpectedType` is surfaced as a marker
654        // violation (`rule_id = "__cel_runtime_error__"`) that the caller
655        // lifts into `ValidationError::runtime_error`.
656        let result = match program.execute(&ctx) {
657            Ok(v) => v,
658            Err(ExecutionError::NoSuchKey(_)) => return Ok(()),
659            Err(e @ ExecutionError::UnexpectedType { .. }) => {
660                // Surface as a runtime-error violation. The caller lifts this
661                // into the enclosing `ValidationError::runtime_error` slot.
662                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    // Helper: coerce a cel arg to i64 if numeric, None if Null/missing.
707    // Hoisted to top to satisfy `items_after_statements`.
708    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    // int() override: support Timestamp → Unix seconds (and pass-through for
728    // other types via Arguments dispatch). cel-interpreter's builtin int()
729    // only handles primitive conversions.
730    // `dyn(x)` — identity pass-through for dynamic typing (cel-go behavior).
731    // Split by Value variant since a Value return type isn't supported.
732    ctx.add_function("dyn", |This(v): This<i64>| -> i64 { v });
733
734    // Override `int()` to support Timestamp → Unix seconds. Use This<Value>
735    // since cel-interpreter treats the first arg to `f(x)` as the receiver.
736    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    // String format checks. Signatures match the canonical cel-go `protovalidate`
753    // library. The rule helpers module owns the implementations.
754    //
755    // The `This<Arc<String>>` receiver pattern enables method-call syntax:
756    // `this.ref_id.isUuid()` — same pattern as `startsWith` / `endsWith` in the
757    // cel-interpreter standard library.
758    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            // port optional: accept bare hostname, IPv4, or [ipv6]/ipv6.
774            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            // Bracketed IPv6 without port: `[::1]`.
781            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    // isIp / isIpPrefix accept 0..=2 optional args (version, strict).
797    // Register with variadic Arguments and dispatch on value types.
798    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            // Each arg may be Null (absent proto3 optional). Try numeric then bool.
814            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                // Position semantics: (ver, strict) or (ver,) or (strict,) depending on types.
822                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}