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<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
114// ---------------------------------------------------------------------------
115// Well-Known Type `AsCelValue` impls.
116//
117// These let plugin-emitted code call
118// `::protovalidate_buffa::cel::AsCelValue::as_cel_value(inner)` for fields of
119// type `google.protobuf.FieldMask`, `google.protobuf.Timestamp`, and
120// `google.protobuf.Duration` — including the predefined-rule path, which binds
121// `this` to the field value via `AsCelValue`.
122//
123// `FieldMask` is exposed as a CEL map with one entry, `paths`, so expressions
124// like `this.paths.all(p, ...)` work. `Timestamp` and `Duration` are exposed
125// as native CEL `Timestamp` / `Duration` values, which carry their own
126// comparison and arithmetic operators in CEL.
127
128impl 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
228/// Generic adapter used by emitted impls: accepts anything that implements `ToCelValue`.
229///
230/// Convert any enum-like value to i32. Works for buffa `EnumValue<E>` and raw
231/// enum types (which can be cast via `Enumeration::to_i32`).
232pub 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/// # Panics
242///
243/// Panics only if the fallback `from_timestamp(0, 0)` fails, which is
244/// impossible (Unix epoch is always representable).
245#[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    /// Evaluates this CEL expression against `this` (bound as the `this` variable
272    /// inside the expression) plus a per-call-frozen `now` timestamp.
273    ///
274    /// # Errors
275    ///
276    /// Returns a [`Violation`] when the compiled CEL expression returns
277    /// `false`, returns a non-empty string, or produces a runtime error.
278    ///
279    /// # Panics
280    ///
281    /// Panics at first call if the CEL expression fails to compile (it is
282    /// baked in at codegen time, so a parse failure indicates a plugin bug).
283    /// Evaluate this CEL expression with `this` bound to the supplied raw
284    /// `crate::cel_core::Value` (used for scalar-field-level CEL rules where
285    /// the "this" target is a primitive, not a message). The violation's
286    /// `field` path is set from `field_path` and `rule` path is set to
287    /// `[cel[index]]` reflecting position in the repeated `FieldRules.cel`.
288    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    /// Like `eval_value_at` but for `(field).cel_expression` (string)
315    /// constraints — the rule path uses `field_number=29`, `field_name="cel_expression"`,
316    /// `field_type=String` instead of cel's `field_number=23`/`field_type=Message`.
317    ///
318    /// # Errors
319    ///
320    /// Returns a [`Violation`] when the CEL expression rejects `this`.
321    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    /// Evaluate for a `repeated.items.cel[idx]` rule. Rule path is
348    /// `[repeated(18), items(4), cel(23, index:cel_idx)]`.
349    ///
350    /// # Errors
351    ///
352    /// Returns a [`Violation`] when the CEL expression rejects `this`.
353    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    /// `map.keys.cel[idx]` — `for_key=true`.
398    ///
399    /// # Errors
400    ///
401    /// Returns a [`Violation`] when the CEL expression rejects `this`.
402    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    /// `map.values.cel[idx]`.
448    ///
449    /// # Errors
450    ///
451    /// Returns a [`Violation`] when the CEL expression rejects `this`.
452    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    /// Evaluate a predefined-rule CEL expression with `this` and `rule`
497    /// bindings. Caller supplies the complete `field_path` and `rule_path`.
498    ///
499    /// # Errors
500    ///
501    /// Returns a [`Violation`] when the CEL expression rejects `this`.
502    ///
503    /// # Panics
504    ///
505    /// Panics if the CEL expression fails to compile (baked in at codegen time).
506    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    /// Evaluate with `this` already bound as a raw CEL [`Value`].
548    ///
549    /// # Errors
550    ///
551    /// Returns a [`Violation`] when the CEL expression rejects `this`.
552    ///
553    /// # Panics
554    ///
555    /// Panics if the CEL expression fails to compile (baked in at codegen time).
556    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    /// Evaluate with `this` bound via the [`AsCelValue`] trait.
587    ///
588    /// # Errors
589    ///
590    /// Returns a [`Violation`] when the CEL expression rejects `this` or when
591    /// a non-skippable runtime error occurs.
592    ///
593    /// # Panics
594    ///
595    /// Panics if the CEL expression fails to compile (baked in at codegen time).
596    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        // Execute. `NoSuchKey` on message-level eval is not a hard error —
611        // protovalidate semantics treat missing submessages as zero-valued,
612        // so we skip the rule. `UnexpectedType` is surfaced as a marker
613        // violation (`rule_id = "__cel_runtime_error__"`) that the caller
614        // lifts into `ValidationError::runtime_error`.
615        let result = match program.execute(&ctx) {
616            Ok(v) => v,
617            Err(ExecutionError::NoSuchKey(_)) => return Ok(()),
618            Err(e @ ExecutionError::UnexpectedType { .. }) => {
619                // Surface as a runtime-error violation. The caller lifts this
620                // into the enclosing `ValidationError::runtime_error` slot.
621                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    // Helper: coerce a cel arg to i64 if numeric, None if Null/missing.
666    // Hoisted to top to satisfy `items_after_statements`.
667    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    // int() override: support Timestamp → Unix seconds (and pass-through for
687    // other types via Arguments dispatch). cel-interpreter's builtin int()
688    // only handles primitive conversions.
689    // `dyn(x)` — identity pass-through for dynamic typing (cel-go behavior).
690    // Split by Value variant since a Value return type isn't supported.
691    ctx.add_function("dyn", |This(v): This<i64>| -> i64 { v });
692
693    // Override `int()` to support Timestamp → Unix seconds. Use This<Value>
694    // since cel-interpreter treats the first arg to `f(x)` as the receiver.
695    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    // String format checks. Signatures match the canonical cel-go `protovalidate`
712    // library. The rule helpers module owns the implementations.
713    //
714    // The `This<Arc<String>>` receiver pattern enables method-call syntax:
715    // `this.ref_id.isUuid()` — same pattern as `startsWith` / `endsWith` in the
716    // cel-interpreter standard library.
717    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            // port optional: accept bare hostname, IPv4, or [ipv6]/ipv6.
733            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            // Bracketed IPv6 without port: `[::1]`.
740            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    // isIp / isIpPrefix accept 0..=2 optional args (version, strict).
756    // Register with variadic Arguments and dispatch on value types.
757    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            // Each arg may be Null (absent proto3 optional). Try numeric then bool.
773            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                // Position semantics: (ver, strict) or (ver,) or (strict,) depending on types.
781                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}