Skip to main content

protovalidate_buffa/
cel.rs

1use std::{
2    borrow::Cow,
3    sync::{Arc, OnceLock},
4};
5
6use cel_interpreter::{
7    extractors::{Arguments, This},
8    Context, Program, Value,
9};
10
11use crate::{FieldPath, Violation};
12
13pub struct CelConstraint {
14    pub id: &'static str,
15    pub message: &'static str,
16    pub expression: &'static str,
17    program: OnceLock<Program>,
18}
19
20pub trait AsCelValue {
21    fn as_cel_value(&self) -> Value;
22}
23
24/// Proto scalar / list → CEL value conversion, used by plugin-emitted `AsCelValue` impls.
25pub trait ToCelValue {
26    fn to_cel_value(&self) -> Value;
27}
28
29impl ToCelValue for String {
30    fn to_cel_value(&self) -> Value {
31        Value::String(self.clone().into())
32    }
33}
34
35impl ToCelValue for str {
36    fn to_cel_value(&self) -> Value {
37        Value::String(self.to_string().into())
38    }
39}
40
41impl ToCelValue for i32 {
42    fn to_cel_value(&self) -> Value {
43        Value::Int(i64::from(*self))
44    }
45}
46
47impl ToCelValue for i64 {
48    fn to_cel_value(&self) -> Value {
49        Value::Int(*self)
50    }
51}
52
53impl ToCelValue for u32 {
54    fn to_cel_value(&self) -> Value {
55        Value::UInt(u64::from(*self))
56    }
57}
58
59impl ToCelValue for u64 {
60    fn to_cel_value(&self) -> Value {
61        Value::UInt(*self)
62    }
63}
64
65impl ToCelValue for f32 {
66    fn to_cel_value(&self) -> Value {
67        Value::Float(f64::from(*self))
68    }
69}
70
71impl ToCelValue for f64 {
72    fn to_cel_value(&self) -> Value {
73        Value::Float(*self)
74    }
75}
76
77impl ToCelValue for bool {
78    fn to_cel_value(&self) -> Value {
79        Value::Bool(*self)
80    }
81}
82
83impl ToCelValue for Vec<u8> {
84    fn to_cel_value(&self) -> Value {
85        Value::Bytes(self.clone().into())
86    }
87}
88
89impl<T: AsCelValue> ToCelValue for Option<T> {
90    fn to_cel_value(&self) -> Value {
91        self.as_ref().map_or(Value::Null, AsCelValue::as_cel_value)
92    }
93}
94
95impl<T: ToCelValue> ToCelValue for Vec<T> {
96    fn to_cel_value(&self) -> Value {
97        Value::List(
98            self.iter()
99                .map(ToCelValue::to_cel_value)
100                .collect::<Vec<_>>()
101                .into(),
102        )
103    }
104}
105
106impl<T: AsCelValue + Default> ToCelValue for buffa::MessageField<T> {
107    fn to_cel_value(&self) -> Value {
108        self.as_option()
109            .map_or(Value::Null, AsCelValue::as_cel_value)
110    }
111}
112
113impl<E: buffa::Enumeration> ToCelValue for buffa::EnumValue<E> {
114    fn to_cel_value(&self) -> Value {
115        Value::Int(i64::from(self.to_i32()))
116    }
117}
118
119macro_rules! impl_to_cel_for_hashmap_key {
120    ($kty:ty => $ktarget:ty) => {
121        impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
122        where
123            V: ToCelValue,
124            S: std::hash::BuildHasher,
125        {
126            fn to_cel_value(&self) -> Value {
127                let map: cel_interpreter::objects::Map = self
128                    .iter()
129                    .map(|(k, v)| {
130                        (
131                            cel_interpreter::objects::Key::from(k.clone() as $ktarget),
132                            v.to_cel_value(),
133                        )
134                    })
135                    .collect::<std::collections::HashMap<_, _>>()
136                    .into();
137                Value::Map(map)
138            }
139        }
140    };
141    (string: $kty:ty) => {
142        impl<V, S> ToCelValue for std::collections::HashMap<$kty, V, S>
143        where
144            V: ToCelValue,
145            S: std::hash::BuildHasher,
146        {
147            fn to_cel_value(&self) -> Value {
148                let map: cel_interpreter::objects::Map = self
149                    .iter()
150                    .map(|(k, v)| {
151                        (
152                            cel_interpreter::objects::Key::from(k.clone()),
153                            v.to_cel_value(),
154                        )
155                    })
156                    .collect::<std::collections::HashMap<_, _>>()
157                    .into();
158                Value::Map(map)
159            }
160        }
161    };
162}
163impl_to_cel_for_hashmap_key!(i32 => i64);
164impl_to_cel_for_hashmap_key!(u32 => u64);
165impl_to_cel_for_hashmap_key!(i64 => i64);
166impl_to_cel_for_hashmap_key!(u64 => u64);
167impl_to_cel_for_hashmap_key!(string: String);
168
169impl<V, S> ToCelValue for std::collections::HashMap<bool, V, S>
170where
171    V: ToCelValue,
172    S: std::hash::BuildHasher,
173{
174    fn to_cel_value(&self) -> Value {
175        let map: cel_interpreter::objects::Map = self
176            .iter()
177            .map(|(k, v)| (cel_interpreter::objects::Key::from(*k), v.to_cel_value()))
178            .collect::<std::collections::HashMap<_, _>>()
179            .into();
180        Value::Map(map)
181    }
182}
183
184/// Generic adapter used by emitted impls: accepts anything that implements `ToCelValue`.
185///
186/// Convert any enum-like value to i32. Works for buffa `EnumValue<E>` and raw
187/// enum types (which can be cast via `Enumeration::to_i32`).
188pub fn enum_to_i32<E: buffa::Enumeration + Copy>(v: &E) -> i32 {
189    v.to_i32()
190}
191
192#[must_use]
193pub fn duration_from_secs_nanos(seconds: i64, nanos: i32) -> chrono::Duration {
194    chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(i64::from(nanos))
195}
196
197/// # Panics
198///
199/// Panics only if the fallback `from_timestamp(0, 0)` fails, which is
200/// impossible (Unix epoch is always representable).
201#[must_use]
202pub fn timestamp_from_secs_nanos(
203    seconds: i64,
204    nanos: i32,
205) -> chrono::DateTime<chrono::FixedOffset> {
206    let nanos_u32 = u32::try_from(nanos.max(0)).unwrap_or(0);
207    let s = chrono::DateTime::<chrono::Utc>::from_timestamp(seconds, nanos_u32)
208        .unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
209    s.fixed_offset()
210}
211
212pub fn to_cel_value<T: ToCelValue + ?Sized>(v: &T) -> Value {
213    v.to_cel_value()
214}
215
216impl CelConstraint {
217    #[must_use]
218    pub const fn new(id: &'static str, message: &'static str, expression: &'static str) -> Self {
219        Self {
220            id,
221            message,
222            expression,
223            program: OnceLock::new(),
224        }
225    }
226
227    /// Evaluates this CEL expression against `this` (bound as the `this` variable
228    /// inside the expression) plus a per-call-frozen `now` timestamp.
229    ///
230    /// # Errors
231    ///
232    /// Returns a [`Violation`] when the compiled CEL expression returns
233    /// `false`, returns a non-empty string, or produces a runtime error.
234    ///
235    /// # Panics
236    ///
237    /// Panics at first call if the CEL expression fails to compile (it is
238    /// baked in at codegen time, so a parse failure indicates a plugin bug).
239    /// Evaluate this CEL expression with `this` bound to the supplied raw
240    /// `cel_interpreter::Value` (used for scalar-field-level CEL rules where
241    /// the "this" target is a primitive, not a message). The violation's
242    /// `field` path is set from `field_path` and `rule` path is set to
243    /// `[cel[index]]` reflecting position in the repeated `FieldRules.cel`.
244    pub fn eval_value_at(
245        &self,
246        this: Value,
247        field_path: FieldPath,
248        cel_index: u64,
249    ) -> Result<(), Violation> {
250        let r = self.eval_value(this);
251        match r {
252            Ok(()) => Ok(()),
253            Err(mut v) => {
254                v.field = field_path;
255                v.rule = FieldPath {
256                    elements: vec![crate::FieldPathElement {
257                        field_number: Some(23),
258                        field_name: Some(Cow::Borrowed("cel")),
259                        field_type: Some(crate::FieldType::Message),
260                        key_type: None,
261                        value_type: None,
262                        subscript: Some(crate::Subscript::Index(cel_index)),
263                    }],
264                };
265                Err(v)
266            }
267        }
268    }
269
270    /// Like `eval_value_at` but for `(field).cel_expression` (string)
271    /// constraints — the rule path uses `field_number=29`, `field_name="cel_expression"`,
272    /// `field_type=String` instead of cel's `field_number=23`/`field_type=Message`.
273    ///
274    /// # Errors
275    ///
276    /// Returns a [`Violation`] when the CEL expression rejects `this`.
277    pub fn eval_expr_value_at(
278        &self,
279        this: Value,
280        field_path: FieldPath,
281        index: u64,
282    ) -> Result<(), Violation> {
283        let r = self.eval_value(this);
284        match r {
285            Ok(()) => Ok(()),
286            Err(mut v) => {
287                v.field = field_path;
288                v.rule = FieldPath {
289                    elements: vec![crate::FieldPathElement {
290                        field_number: Some(29),
291                        field_name: Some(Cow::Borrowed("cel_expression")),
292                        field_type: Some(crate::FieldType::String),
293                        key_type: None,
294                        value_type: None,
295                        subscript: Some(crate::Subscript::Index(index)),
296                    }],
297                };
298                Err(v)
299            }
300        }
301    }
302
303    /// Evaluate for a `repeated.items.cel[idx]` rule. Rule path is
304    /// `[repeated(18), items(4), cel(23, index:cel_idx)]`.
305    ///
306    /// # Errors
307    ///
308    /// Returns a [`Violation`] when the CEL expression rejects `this`.
309    pub fn eval_repeated_items_cel(
310        &self,
311        this: Value,
312        field_path: FieldPath,
313        cel_idx: u64,
314    ) -> Result<(), Violation> {
315        let r = self.eval_value(this);
316        match r {
317            Ok(()) => Ok(()),
318            Err(mut v) => {
319                v.field = field_path;
320                v.rule = FieldPath {
321                    elements: vec![
322                        crate::FieldPathElement {
323                            field_number: Some(18),
324                            field_name: Some(Cow::Borrowed("repeated")),
325                            field_type: Some(crate::FieldType::Message),
326                            key_type: None,
327                            value_type: None,
328                            subscript: None,
329                        },
330                        crate::FieldPathElement {
331                            field_number: Some(4),
332                            field_name: Some(Cow::Borrowed("items")),
333                            field_type: Some(crate::FieldType::Message),
334                            key_type: None,
335                            value_type: None,
336                            subscript: None,
337                        },
338                        crate::FieldPathElement {
339                            field_number: Some(23),
340                            field_name: Some(Cow::Borrowed("cel")),
341                            field_type: Some(crate::FieldType::Message),
342                            key_type: None,
343                            value_type: None,
344                            subscript: Some(crate::Subscript::Index(cel_idx)),
345                        },
346                    ],
347                };
348                Err(v)
349            }
350        }
351    }
352
353    /// `map.keys.cel[idx]` — `for_key=true`.
354    ///
355    /// # Errors
356    ///
357    /// Returns a [`Violation`] when the CEL expression rejects `this`.
358    pub fn eval_map_keys_cel(
359        &self,
360        this: Value,
361        field_path: FieldPath,
362        cel_idx: u64,
363    ) -> Result<(), Violation> {
364        let r = self.eval_value(this);
365        match r {
366            Ok(()) => Ok(()),
367            Err(mut v) => {
368                v.field = field_path;
369                v.for_key = true;
370                v.rule = FieldPath {
371                    elements: vec![
372                        crate::FieldPathElement {
373                            field_number: Some(19),
374                            field_name: Some(Cow::Borrowed("map")),
375                            field_type: Some(crate::FieldType::Message),
376                            key_type: None,
377                            value_type: None,
378                            subscript: None,
379                        },
380                        crate::FieldPathElement {
381                            field_number: Some(4),
382                            field_name: Some(Cow::Borrowed("keys")),
383                            field_type: Some(crate::FieldType::Message),
384                            key_type: None,
385                            value_type: None,
386                            subscript: None,
387                        },
388                        crate::FieldPathElement {
389                            field_number: Some(23),
390                            field_name: Some(Cow::Borrowed("cel")),
391                            field_type: Some(crate::FieldType::Message),
392                            key_type: None,
393                            value_type: None,
394                            subscript: Some(crate::Subscript::Index(cel_idx)),
395                        },
396                    ],
397                };
398                Err(v)
399            }
400        }
401    }
402
403    /// `map.values.cel[idx]`.
404    ///
405    /// # Errors
406    ///
407    /// Returns a [`Violation`] when the CEL expression rejects `this`.
408    pub fn eval_map_values_cel(
409        &self,
410        this: Value,
411        field_path: FieldPath,
412        cel_idx: u64,
413    ) -> Result<(), Violation> {
414        let r = self.eval_value(this);
415        match r {
416            Ok(()) => Ok(()),
417            Err(mut v) => {
418                v.field = field_path;
419                v.rule = FieldPath {
420                    elements: vec![
421                        crate::FieldPathElement {
422                            field_number: Some(19),
423                            field_name: Some(Cow::Borrowed("map")),
424                            field_type: Some(crate::FieldType::Message),
425                            key_type: None,
426                            value_type: None,
427                            subscript: None,
428                        },
429                        crate::FieldPathElement {
430                            field_number: Some(5),
431                            field_name: Some(Cow::Borrowed("values")),
432                            field_type: Some(crate::FieldType::Message),
433                            key_type: None,
434                            value_type: None,
435                            subscript: None,
436                        },
437                        crate::FieldPathElement {
438                            field_number: Some(23),
439                            field_name: Some(Cow::Borrowed("cel")),
440                            field_type: Some(crate::FieldType::Message),
441                            key_type: None,
442                            value_type: None,
443                            subscript: Some(crate::Subscript::Index(cel_idx)),
444                        },
445                    ],
446                };
447                Err(v)
448            }
449        }
450    }
451
452    /// Evaluate a predefined-rule CEL expression with `this` and `rule`
453    /// bindings. Caller supplies the complete `field_path` and `rule_path`.
454    ///
455    /// # Errors
456    ///
457    /// Returns a [`Violation`] when the CEL expression rejects `this`.
458    ///
459    /// # Panics
460    ///
461    /// Panics if the CEL expression fails to compile (baked in at codegen time).
462    pub fn eval_predefined(
463        &self,
464        this: Value,
465        rule: Value,
466        field_path: FieldPath,
467        rule_path: FieldPath,
468    ) -> Result<(), Violation> {
469        let program = self.program.get_or_init(|| {
470            Program::compile(self.expression)
471                .unwrap_or_else(|e| panic!("CEL compile failed for {}: {e}", self.id))
472        });
473        let mut ctx = Context::default();
474        ctx.add_variable("this", this).expect("cel: 'this'");
475        ctx.add_variable("rule", rule).expect("cel: 'rule'");
476        ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
477            .expect("cel: 'now'");
478        register_custom_functions(&mut ctx);
479        let result = program.execute(&ctx).map_err(|e| Violation {
480            field: field_path.clone(),
481            rule: rule_path.clone(),
482            rule_id: Cow::Borrowed(self.id),
483            message: Cow::Owned(format!("cel runtime error: {e}")),
484            for_key: false,
485        })?;
486        let ok = match result {
487            Value::Bool(true) => true,
488            Value::String(s) if s.is_empty() => true,
489            _ => false,
490        };
491        if ok {
492            return Ok(());
493        }
494        Err(Violation {
495            field: field_path,
496            rule: rule_path,
497            rule_id: Cow::Borrowed(self.id),
498            message: Cow::Borrowed(self.message),
499            for_key: false,
500        })
501    }
502
503    /// Evaluate with `this` already bound as a raw CEL [`Value`].
504    ///
505    /// # Errors
506    ///
507    /// Returns a [`Violation`] when the CEL expression rejects `this`.
508    ///
509    /// # Panics
510    ///
511    /// Panics if the CEL expression fails to compile (baked in at codegen time).
512    pub fn eval_value(&self, this: Value) -> 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' binding");
519        ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
520            .expect("cel: 'now' binding");
521        register_custom_functions(&mut ctx);
522        let result = program
523            .execute(&ctx)
524            .map_err(|e| self.violation(Cow::Owned(format!("cel runtime error: {e}"))))?;
525        match result {
526            Value::Bool(true) => Ok(()),
527            Value::String(s) if s.is_empty() => Ok(()),
528            Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
529            Value::String(s) => {
530                if self.message.is_empty() {
531                    Err(self.violation(Cow::Owned(s.to_string())))
532                } else {
533                    Err(self.violation(Cow::Borrowed(self.message)))
534                }
535            }
536            other => Err(self.violation(Cow::Owned(format!(
537                "cel returned non-bool/string: {other:?}"
538            )))),
539        }
540    }
541
542    /// Evaluate with `this` bound via the [`AsCelValue`] trait.
543    ///
544    /// # Errors
545    ///
546    /// Returns a [`Violation`] when the CEL expression rejects `this` or when
547    /// a non-skippable runtime error occurs.
548    ///
549    /// # Panics
550    ///
551    /// Panics if the CEL expression fails to compile (baked in at codegen time).
552    pub fn eval<T: AsCelValue>(&self, this: &T) -> Result<(), Violation> {
553        use cel_interpreter::ExecutionError;
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
559        let mut ctx = Context::default();
560        ctx.add_variable("this", this.as_cel_value())
561            .expect("cel: 'this' binding");
562        ctx.add_variable("now", Value::Timestamp(chrono::Utc::now().fixed_offset()))
563            .expect("cel: 'now' binding");
564        register_custom_functions(&mut ctx);
565
566        // Execute. `NoSuchKey` on message-level eval is not a hard error —
567        // protovalidate semantics treat missing submessages as zero-valued,
568        // so we skip the rule. `UnexpectedType` is surfaced as a marker
569        // violation (`rule_id = "__cel_runtime_error__"`) that the caller
570        // lifts into `ValidationError::runtime_error`.
571        let result = match program.execute(&ctx) {
572            Ok(v) => v,
573            Err(ExecutionError::NoSuchKey(_)) => return Ok(()),
574            Err(e @ ExecutionError::UnexpectedType { .. }) => {
575                // Surface as a runtime-error violation. The caller lifts this
576                // into the enclosing `ValidationError::runtime_error` slot.
577                return Err(Violation {
578                    field: FieldPath::default(),
579                    rule: FieldPath::default(),
580                    rule_id: Cow::Borrowed("__cel_runtime_error__"),
581                    message: Cow::Owned(e.to_string()),
582                    for_key: false,
583                });
584            }
585            Err(e) => return Err(self.violation(Cow::Owned(format!("cel runtime error: {e}")))),
586        };
587
588        match result {
589            Value::Bool(true) => Ok(()),
590            Value::String(s) if s.is_empty() => Ok(()),
591            Value::Bool(false) => Err(self.violation(Cow::Borrowed(self.message))),
592            Value::String(s) => {
593                if self.message.is_empty() {
594                    Err(self.violation(Cow::Owned(s.to_string())))
595                } else {
596                    Err(self.violation(Cow::Borrowed(self.message)))
597                }
598            }
599            other => Err(self.violation(Cow::Owned(format!(
600                "cel returned non-bool/string: {other:?}"
601            )))),
602        }
603    }
604
605    fn violation(&self, message: Cow<'static, str>) -> Violation {
606        Violation {
607            field: FieldPath::default(),
608            rule: FieldPath::default(),
609            rule_id: Cow::Borrowed(self.id),
610            message,
611            for_key: false,
612        }
613    }
614}
615
616#[expect(
617    clippy::too_many_lines,
618    reason = "one registration per CEL function — splitting scatters related registrations"
619)]
620fn register_custom_functions(ctx: &mut Context<'_>) {
621    // Helper: coerce a cel arg to i64 if numeric, None if Null/missing.
622    // Hoisted to top to satisfy `items_after_statements`.
623    const fn arg_i64(v: Option<&Value>) -> Option<i64> {
624        match v {
625            Some(Value::Int(n)) => Some(*n),
626            #[expect(
627                clippy::cast_possible_wrap,
628                reason = "CEL coerces u64 → i64 per spec; wrap is intended"
629            )]
630            Some(Value::UInt(n)) => Some(*n as i64),
631            _ => None,
632        }
633    }
634    const fn arg_bool(v: Option<&Value>) -> Option<bool> {
635        if let Some(Value::Bool(b)) = v {
636            Some(*b)
637        } else {
638            None
639        }
640    }
641
642    // int() override: support Timestamp → Unix seconds (and pass-through for
643    // other types via Arguments dispatch). cel-interpreter's builtin int()
644    // only handles primitive conversions.
645    // `dyn(x)` — identity pass-through for dynamic typing (cel-go behavior).
646    // Split by Value variant since a Value return type isn't supported.
647    ctx.add_function("dyn", |This(v): This<i64>| -> i64 { v });
648
649    // Override `int()` to support Timestamp → Unix seconds. Use This<Value>
650    // since cel-interpreter treats the first arg to `f(x)` as the receiver.
651    ctx.add_function("int", |This(v): This<Value>| -> i64 {
652        match v {
653            Value::Timestamp(t) => t.timestamp(),
654            Value::Int(i) => i,
655            #[expect(clippy::cast_possible_wrap, reason = "CEL int() on u64 wraps per spec")]
656            Value::UInt(u) => u as i64,
657            #[expect(
658                clippy::cast_possible_truncation,
659                reason = "CEL int() truncates float per spec"
660            )]
661            Value::Float(f) => f as i64,
662            Value::String(s) => s.parse::<i64>().unwrap_or(0),
663            Value::Bool(b) => i64::from(b),
664            _ => 0,
665        }
666    });
667    // String format checks. Signatures match the canonical cel-go `protovalidate`
668    // library. The rule helpers module owns the implementations.
669    //
670    // The `This<Arc<String>>` receiver pattern enables method-call syntax:
671    // `this.ref_id.isUuid()` — same pattern as `startsWith` / `endsWith` in the
672    // cel-interpreter standard library.
673    ctx.add_function("isUuid", |This(this): This<Arc<String>>| -> bool {
674        crate::rules::string::is_uuid(&this)
675    });
676    ctx.add_function("isHostname", |This(this): This<Arc<String>>| -> bool {
677        crate::rules::string::is_hostname(&this)
678    });
679    ctx.add_function(
680        "isHostAndPort",
681        |This(this): This<Arc<String>>, port_required: bool| -> bool {
682            if crate::rules::string::is_host_and_port(&this) {
683                return true;
684            }
685            if port_required {
686                return false;
687            }
688            // port optional: accept bare hostname, IPv4, or [ipv6]/ipv6.
689            if crate::rules::string::is_hostname(&this)
690                || crate::rules::string::is_ipv4(&this)
691                || crate::rules::string::is_ipv6(&this)
692            {
693                return true;
694            }
695            // Bracketed IPv6 without port: `[::1]`.
696            if let Some(inner) = this.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
697                return crate::rules::string::is_ipv6(inner);
698            }
699            false
700        },
701    );
702    ctx.add_function("isEmail", |This(this): This<Arc<String>>| -> bool {
703        crate::rules::string::is_email(&this)
704    });
705    ctx.add_function("isUri", |This(this): This<Arc<String>>| -> bool {
706        crate::rules::string::is_uri(&this)
707    });
708    ctx.add_function("isUriRef", |This(this): This<Arc<String>>| -> bool {
709        crate::rules::string::is_uri_ref(&this)
710    });
711    // isIp / isIpPrefix accept 0..=2 optional args (version, strict).
712    // Register with variadic Arguments and dispatch on value types.
713    ctx.add_function(
714        "isIp",
715        |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
716            let ver = arg_i64(args.first()).unwrap_or(0);
717            match ver {
718                0 => crate::rules::string::is_ip(&this),
719                4 => crate::rules::string::is_ipv4(&this),
720                6 => crate::rules::string::is_ipv6(&this),
721                _ => false,
722            }
723        },
724    );
725    ctx.add_function(
726        "isIpPrefix",
727        |This(this): This<Arc<String>>, Arguments(args): Arguments| -> bool {
728            // Each arg may be Null (absent proto3 optional). Try numeric then bool.
729            let (ver, strict) = {
730                let a0 = args.first();
731                let a1 = args.get(1);
732                let v_i = arg_i64(a0);
733                let v_b = arg_bool(a0);
734                let i_i = arg_i64(a1);
735                let i_b = arg_bool(a1);
736                // Position semantics: (ver, strict) or (ver,) or (strict,) depending on types.
737                if let (Some(n), Some(b)) = (v_i, i_b) {
738                    (n, Some(b))
739                } else if let Some(n) = v_i {
740                    (n, i_b)
741                } else if let Some(b) = v_b {
742                    (i_i.unwrap_or(0), Some(b))
743                } else {
744                    (0, i_b)
745                }
746            };
747            let strict = strict.unwrap_or(false);
748            let addr_ok = match ver {
749                0 => true,
750                4 => {
751                    this.parse::<::std::net::Ipv4Addr>().is_ok()
752                        || crate::rules::string::is_ipv4_with_prefixlen(&this)
753                }
754                6 => {
755                    this.parse::<::std::net::Ipv6Addr>().is_ok()
756                        || crate::rules::string::is_ipv6_with_prefixlen(&this)
757                }
758                _ => return false,
759            };
760            if !addr_ok {
761                return false;
762            }
763            if strict {
764                match ver {
765                    4 => crate::rules::string::is_ipv4_prefix(&this),
766                    6 => crate::rules::string::is_ipv6_prefix(&this),
767                    _ => crate::rules::string::is_ip_prefix(&this),
768                }
769            } else {
770                match ver {
771                    4 => crate::rules::string::is_ipv4_with_prefixlen(&this),
772                    6 => crate::rules::string::is_ipv6_with_prefixlen(&this),
773                    _ => crate::rules::string::is_ip_with_prefixlen(&this),
774                }
775            }
776        },
777    );
778}