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