Skip to main content

prost_protovalidate_types/
rules_meta.rs

1//! Canonical rule metadata shared by the runtime validator and the
2//! build-time code generator.
3//!
4//! This module is the single source of truth for `buf.validate` rule
5//! identifiers, violation message text, and rule-combination tables that
6//! both validation engines must agree on byte-for-byte. The runtime crate
7//! (`prost-protovalidate`) consumes it while compiling evaluators; the build
8//! crate (`prost-protovalidate-build`) consumes it while emitting generated
9//! validators, embedding the results as literals.
10//!
11//! Message text tracks the upstream protovalidate conformance corpus and may
12//! change in minor releases; it is not a stability surface of its own.
13
14/// Metadata shared by all twelve proto numeric types
15/// (`int32`…`sfixed64`, `float`, `double`).
16pub mod numeric {
17    use std::fmt::Display;
18
19    /// The violation message for `<numeric>.in`.
20    pub const IN_MESSAGE: &str = "value must be in list";
21    /// The violation message for `<numeric>.not_in`.
22    pub const NOT_IN_MESSAGE: &str = "value must not be in list";
23    /// The violation message for `float.finite` / `double.finite`.
24    pub const FINITE_MESSAGE: &str = "value must be finite";
25
26    /// Rule id for `const`, e.g. `int32.const`.
27    #[must_use]
28    pub fn const_id(prefix: &str) -> String {
29        format!("{prefix}.const")
30    }
31
32    /// Rule id for `in`, e.g. `int32.in`.
33    #[must_use]
34    pub fn in_id(prefix: &str) -> String {
35        format!("{prefix}.in")
36    }
37
38    /// Rule id for `not_in`, e.g. `int32.not_in`.
39    #[must_use]
40    pub fn not_in_id(prefix: &str) -> String {
41        format!("{prefix}.not_in")
42    }
43
44    /// Rule id for `finite`, e.g. `float.finite`.
45    #[must_use]
46    pub fn finite_id(prefix: &str) -> String {
47        format!("{prefix}.finite")
48    }
49
50    /// The violation message for `const`.
51    #[must_use]
52    pub fn const_message<T: Display>(want: T) -> String {
53        format!("value must equal {want}")
54    }
55
56    /// How the `gt`/`gte` and `lt`/`lte` bounds combine.
57    ///
58    /// When both sides are present and the lower bound lies below the upper
59    /// bound, the range is inclusive (value must satisfy both, message joins
60    /// with "and"); when inverted, the range is exclusive (value must
61    /// satisfy either, message joins with "or"). [`range_rule`] selects the
62    /// variant from the bounds; evaluation applies the matching predicate
63    /// through [`RangeKind::violated`].
64    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
65    pub enum RangeKind {
66        /// `gt < lt`: value must be `> gt` and `< lt`.
67        GtLt,
68        /// `gt >= lt`: value must be `> gt` or `< lt`.
69        GtLtExclusive,
70        /// `gt < lte`: value must be `> gt` and `<= lte`.
71        GtLte,
72        /// `gt >= lte`: value must be `> gt` or `<= lte`.
73        GtLteExclusive,
74        /// `gte < lt`: value must be `>= gte` and `< lt`.
75        GteLt,
76        /// `gte >= lt`: value must be `>= gte` or `< lt`.
77        GteLtExclusive,
78        /// `gte <= lte`: value must be `>= gte` and `<= lte`.
79        GteLte,
80        /// `gte > lte`: value must be `>= gte` or `<= lte`.
81        GteLteExclusive,
82        /// Only `gt` is set.
83        Gt,
84        /// Only `gte` is set.
85        Gte,
86        /// Only `lt` is set.
87        Lt,
88        /// Only `lte` is set.
89        Lte,
90    }
91
92    impl RangeKind {
93        /// Whether `v` violates this range. `gt` carries the `gt`/`gte`
94        /// bound, `lt` the `lt`/`lte` bound; a variant ignores bounds it
95        /// does not use and reports no violation when a bound it needs is
96        /// missing.
97        ///
98        /// The build-time code generator emits the token-level equivalent of
99        /// this predicate; the parity suites pin the correspondence.
100        #[must_use]
101        pub fn violated<T: PartialOrd + Copy>(self, gt: Option<T>, lt: Option<T>, v: T) -> bool {
102            match self {
103                Self::Gt => gt.is_some_and(|g| v <= g),
104                Self::Gte => gt.is_some_and(|g| v < g),
105                Self::Lt => lt.is_some_and(|l| v >= l),
106                Self::Lte => lt.is_some_and(|l| v > l),
107                Self::GtLt => gt.zip(lt).is_some_and(|(g, l)| v <= g || v >= l),
108                Self::GtLtExclusive => gt.zip(lt).is_some_and(|(g, l)| v >= l && v <= g),
109                Self::GtLte => gt.zip(lt).is_some_and(|(g, l)| v <= g || v > l),
110                Self::GtLteExclusive => gt.zip(lt).is_some_and(|(g, l)| v > l && v <= g),
111                Self::GteLt => gt.zip(lt).is_some_and(|(g, l)| v < g || v >= l),
112                Self::GteLtExclusive => gt.zip(lt).is_some_and(|(g, l)| v >= l && v < g),
113                Self::GteLte => gt.zip(lt).is_some_and(|(g, l)| v < g || v > l),
114                Self::GteLteExclusive => gt.zip(lt).is_some_and(|(g, l)| v > l && v < g),
115            }
116        }
117    }
118
119    /// A fully resolved range rule: the combination variant plus the exact
120    /// identifier, rule path, and message text both engines must emit.
121    #[derive(Debug, Clone)]
122    pub struct RangeRule {
123        /// Selected bound combination.
124        pub kind: RangeKind,
125        /// e.g. `int32.gte_lte` or `float.gt`.
126        pub rule_id: String,
127        /// For combined bounds, the `greater_than` side (`<prefix>.gt` /
128        /// `<prefix>.gte`); equals `rule_id` for single bounds.
129        pub rule_path: String,
130        /// Human-readable violation message with the bounds rendered in.
131        pub message: String,
132    }
133
134    /// Resolve the range rule for a set of bounds, or `None` when no bound
135    /// is set.
136    ///
137    /// `fmt` renders a bound into the message; pass a formatter over the
138    /// exact wire scalar (`f32` for `float` rules — widening to `f64` first
139    /// changes the rendered digits).
140    #[must_use]
141    pub fn range_rule<T: PartialOrd + Copy>(
142        prefix: &str,
143        gt: Option<T>,
144        gte: Option<T>,
145        lt: Option<T>,
146        lte: Option<T>,
147        fmt: impl Fn(&T) -> String,
148    ) -> Option<RangeRule> {
149        let (g_label, g_desc, g_bound, g_is_eq) = match (&gt, &gte) {
150            (Some(g), None) => ("gt", "greater than", Some(g), false),
151            (None, Some(g)) => ("gte", "greater than or equal to", Some(g), true),
152            _ => ("", "", None, false),
153        };
154        let (l_label, l_desc, l_bound, l_is_eq) = match (&lt, &lte) {
155            (Some(l), None) => ("lt", "less than", Some(l), false),
156            (None, Some(l)) => ("lte", "less than or equal to", Some(l), true),
157            _ => ("", "", None, false),
158        };
159
160        match (g_bound, l_bound) {
161            (Some(g), Some(l)) => {
162                // `gte`+`lte` treats equal bounds as inclusive; other
163                // combinations require strict ordering.
164                let inclusive = if g_is_eq && l_is_eq { g <= l } else { g < l };
165                let kind = match (g_is_eq, l_is_eq, inclusive) {
166                    (false, false, true) => RangeKind::GtLt,
167                    (false, false, false) => RangeKind::GtLtExclusive,
168                    (false, true, true) => RangeKind::GtLte,
169                    (false, true, false) => RangeKind::GtLteExclusive,
170                    (true, false, true) => RangeKind::GteLt,
171                    (true, false, false) => RangeKind::GteLtExclusive,
172                    (true, true, true) => RangeKind::GteLte,
173                    (true, true, false) => RangeKind::GteLteExclusive,
174                };
175                let (suffix, joiner) = if inclusive {
176                    ("", "and")
177                } else {
178                    ("_exclusive", "or")
179                };
180                Some(RangeRule {
181                    kind,
182                    rule_id: format!("{prefix}.{g_label}_{l_label}{suffix}"),
183                    rule_path: format!("{prefix}.{g_label}"),
184                    message: format!(
185                        "value must be {g_desc} {} {joiner} {l_desc} {}",
186                        fmt(g),
187                        fmt(l)
188                    ),
189                })
190            }
191            (Some(g), None) => Some(RangeRule {
192                kind: if g_is_eq {
193                    RangeKind::Gte
194                } else {
195                    RangeKind::Gt
196                },
197                rule_id: format!("{prefix}.{g_label}"),
198                rule_path: format!("{prefix}.{g_label}"),
199                message: format!("value must be {g_desc} {}", fmt(g)),
200            }),
201            (None, Some(l)) => Some(RangeRule {
202                kind: if l_is_eq {
203                    RangeKind::Lte
204                } else {
205                    RangeKind::Lt
206                },
207                rule_id: format!("{prefix}.{l_label}"),
208                rule_path: format!("{prefix}.{l_label}"),
209                message: format!("value must be {l_desc} {}", fmt(l)),
210            }),
211            (None, None) => None,
212        }
213    }
214
215    /// The `(rule_id, rule_path)` pair a NaN value produces when it hits a
216    /// range constraint (the message is intentionally empty). `None` when no
217    /// bound is set.
218    #[must_use]
219    pub fn nan_range_rule<T: PartialOrd + Copy>(
220        prefix: &str,
221        gt: Option<T>,
222        gte: Option<T>,
223        lt: Option<T>,
224        lte: Option<T>,
225    ) -> Option<(String, String)> {
226        let rule = range_rule(prefix, gt, gte, lt, lte, |_| String::new())?;
227        Some((rule.rule_id, rule.rule_path))
228    }
229
230    #[cfg(test)]
231    mod tests {
232        use super::{RangeKind, nan_range_rule, range_rule};
233
234        #[test]
235        fn combined_bounds_select_inclusive_and_exclusive_variants() {
236            let incl = range_rule("int32", Some(1), None, Some(10), None, i32::to_string)
237                .expect("bounds set");
238            assert_eq!(incl.kind, RangeKind::GtLt);
239            assert_eq!(incl.rule_id, "int32.gt_lt");
240            assert_eq!(incl.rule_path, "int32.gt");
241            assert_eq!(
242                incl.message,
243                "value must be greater than 1 and less than 10"
244            );
245
246            let excl = range_rule("int32", Some(10), None, Some(1), None, i32::to_string)
247                .expect("bounds set");
248            assert_eq!(excl.kind, RangeKind::GtLtExclusive);
249            assert_eq!(excl.rule_id, "int32.gt_lt_exclusive");
250            assert_eq!(excl.message, "value must be greater than 10 or less than 1");
251        }
252
253        #[test]
254        fn gte_lte_equal_bounds_are_inclusive() {
255            let rule = range_rule("uint32", None, Some(5u32), None, Some(5), u32::to_string)
256                .expect("bounds set");
257            assert_eq!(rule.kind, RangeKind::GteLte);
258            assert_eq!(rule.rule_id, "uint32.gte_lte");
259            assert_eq!(
260                rule.message,
261                "value must be greater than or equal to 5 and less than or equal to 5"
262            );
263        }
264
265        #[test]
266        fn single_bounds_use_their_own_id_as_rule_path() {
267            let rule = range_rule("double", None, None, None, Some(1.5f64), f64::to_string)
268                .expect("bound set");
269            assert_eq!(rule.kind, RangeKind::Lte);
270            assert_eq!(rule.rule_id, "double.lte");
271            assert_eq!(rule.rule_path, "double.lte");
272            assert_eq!(rule.message, "value must be less than or equal to 1.5");
273        }
274
275        #[test]
276        fn float_bounds_format_from_the_wire_scalar() {
277            // 0.1f32 renders as "0.1"; widening to f64 first would render
278            // the f64-widened value instead — the trap this API prevents.
279            let rule = range_rule("float", Some(0.1f32), None, None, None, f32::to_string)
280                .expect("bound set");
281            assert_eq!(rule.message, "value must be greater than 0.1");
282        }
283
284        #[test]
285        fn violated_matches_bound_semantics() {
286            assert!(RangeKind::Gt.violated(Some(5), None, 5));
287            assert!(!RangeKind::Gt.violated(Some(5), None, 6));
288            assert!(RangeKind::Gte.violated(Some(5), None, 4));
289            assert!(!RangeKind::Gte.violated(Some(5), None, 5));
290            assert!(RangeKind::Lt.violated(None, Some(5), 5));
291            assert!(RangeKind::Lte.violated(None, Some(5), 6));
292
293            // Inclusive 1..10 (gt/lt): 1 and 10 violate, 5 passes.
294            assert!(RangeKind::GtLt.violated(Some(1), Some(10), 1));
295            assert!(RangeKind::GtLt.violated(Some(1), Some(10), 10));
296            assert!(!RangeKind::GtLt.violated(Some(1), Some(10), 5));
297
298            // Exclusive gt 10 / lt 1: 5 violates, 0 and 11 pass.
299            assert!(RangeKind::GtLtExclusive.violated(Some(10), Some(1), 5));
300            assert!(!RangeKind::GtLtExclusive.violated(Some(10), Some(1), 0));
301            assert!(!RangeKind::GtLtExclusive.violated(Some(10), Some(1), 11));
302
303            // gte 0 / lte 1 inclusive.
304            assert!(RangeKind::GteLte.violated(Some(0.0), Some(1.0), -0.5));
305            assert!(RangeKind::GteLte.violated(Some(0.0), Some(1.0), 1.5));
306            assert!(!RangeKind::GteLte.violated(Some(0.0), Some(1.0), 0.0));
307        }
308
309        #[test]
310        fn nan_rule_reuses_the_range_table() {
311            let (id, path) =
312                nan_range_rule("float", None, Some(0.0f32), None, Some(100.0)).expect("bounds");
313            assert_eq!(id, "float.gte_lte");
314            assert_eq!(path, "float.gte");
315
316            let (id, path) =
317                nan_range_rule("double", Some(0.0f64), None, None, None).expect("bound");
318            assert_eq!(id, "double.gt");
319            assert_eq!(path, "double.gt");
320
321            assert!(nan_range_rule::<f32>("float", None, None, None, None).is_none());
322        }
323    }
324}
325
326/// Metadata for `google.protobuf.Duration` rules.
327pub mod duration {
328    use super::numeric;
329
330    /// Rule id for `duration.const`.
331    pub const CONST_ID: &str = "duration.const";
332    /// Rule id for `duration.in`.
333    pub const IN_ID: &str = "duration.in";
334    /// Rule id for `duration.not_in`.
335    pub const NOT_IN_ID: &str = "duration.not_in";
336
337    /// Render a duration Go-style, as used in every duration message
338    /// (`"3s"`, `"1.5s"`, `"-2s"`).
339    #[must_use]
340    pub fn fmt(seconds: i64, nanos: i32) -> String {
341        if nanos == 0 {
342            format!("{seconds}s")
343        } else {
344            let sign = if seconds < 0 || nanos < 0 { "-" } else { "" };
345            let secs = seconds.unsigned_abs();
346            let nanos = nanos.unsigned_abs();
347            let frac = format!("{nanos:09}");
348            let frac = frac.trim_end_matches('0');
349            format!("{sign}{secs}.{frac}s")
350        }
351    }
352
353    /// The violation message for `duration.const`.
354    #[must_use]
355    pub fn const_message(seconds: i64, nanos: i32) -> String {
356        format!("value must equal {}", fmt(seconds, nanos))
357    }
358
359    /// The violation message for `duration.in`.
360    #[must_use]
361    pub fn in_message(items: &[(i64, i32)]) -> String {
362        format!("value must be in list [{}]", join(items))
363    }
364
365    /// The violation message for `duration.not_in`.
366    #[must_use]
367    pub fn not_in_message(items: &[(i64, i32)]) -> String {
368        format!("value must not be in list [{}]", join(items))
369    }
370
371    fn join(items: &[(i64, i32)]) -> String {
372        items
373            .iter()
374            .map(|(s, n)| fmt(*s, *n))
375            .collect::<Vec<_>>()
376            .join(", ")
377    }
378
379    /// Resolve the range rule for `(seconds, nanos)` bounds.
380    ///
381    /// Duration ordering is lexicographic on the tuple (protobuf requires
382    /// `seconds` and `nanos` to share a sign), so the shared
383    /// [`numeric::RangeKind::violated`] predicate applies to the tuples
384    /// directly.
385    #[must_use]
386    pub fn range_rule(
387        gt: Option<(i64, i32)>,
388        gte: Option<(i64, i32)>,
389        lt: Option<(i64, i32)>,
390        lte: Option<(i64, i32)>,
391    ) -> Option<numeric::RangeRule> {
392        numeric::range_rule("duration", gt, gte, lt, lte, |(s, n)| fmt(*s, *n))
393    }
394
395    #[cfg(test)]
396    mod tests {
397        use super::{fmt, in_message, range_rule};
398
399        #[test]
400        fn fmt_matches_go_style() {
401            assert_eq!(fmt(3, 0), "3s");
402            assert_eq!(fmt(1, 500_000_000), "1.5s");
403            assert_eq!(fmt(-2, 0), "-2s");
404            assert_eq!(fmt(0, -500_000_000), "-0.5s");
405            assert_eq!(fmt(0, 1), "0.000000001s");
406        }
407
408        #[test]
409        fn range_messages_render_bounds_go_style() {
410            let rule = range_rule(Some((0, 0)), None, Some((60, 0)), None).expect("bounds");
411            assert_eq!(rule.rule_id, "duration.gt_lt");
412            assert_eq!(
413                rule.message,
414                "value must be greater than 0s and less than 60s"
415            );
416        }
417
418        #[test]
419        fn list_messages_join_with_brackets() {
420            assert_eq!(
421                in_message(&[(1, 0), (2, 500_000_000)]),
422                "value must be in list [1s, 2.5s]"
423            );
424        }
425    }
426}
427
428/// Metadata for `google.protobuf.Timestamp` rules.
429pub mod timestamp {
430    use super::numeric;
431
432    /// Rule id for `timestamp.const`.
433    pub const CONST_ID: &str = "timestamp.const";
434    /// The violation message for `timestamp.const` (timestamp messages do
435    /// not render the bound).
436    pub const CONST_MESSAGE: &str = "must equal const timestamp";
437    /// Rule id for `timestamp.lt_now`.
438    pub const LT_NOW_ID: &str = "timestamp.lt_now";
439    /// The violation message for `timestamp.lt_now`.
440    pub const LT_NOW_MESSAGE: &str = "must be less than now";
441    /// Rule id for `timestamp.gt_now`.
442    pub const GT_NOW_ID: &str = "timestamp.gt_now";
443    /// The violation message for `timestamp.gt_now`.
444    pub const GT_NOW_MESSAGE: &str = "must be greater than now";
445    /// Rule id for `timestamp.within`.
446    pub const WITHIN_ID: &str = "timestamp.within";
447    /// The violation message for `timestamp.within`.
448    pub const WITHIN_MESSAGE: &str = "must be within specified duration of now";
449
450    /// The fixed violation message for each bound combination.
451    #[must_use]
452    pub fn range_message(kind: numeric::RangeKind) -> &'static str {
453        match kind {
454            numeric::RangeKind::GtLt => "must be greater than and less than specified timestamps",
455            numeric::RangeKind::GtLtExclusive => {
456                "must be greater than or less than specified timestamps"
457            }
458            numeric::RangeKind::GtLte => {
459                "must be greater than and less than or equal to specified timestamps"
460            }
461            numeric::RangeKind::GtLteExclusive => {
462                "must be greater than or less than or equal to specified timestamps"
463            }
464            numeric::RangeKind::GteLt => {
465                "must be greater than or equal to and less than specified timestamps"
466            }
467            numeric::RangeKind::GteLtExclusive => {
468                "must be greater than or equal to or less than specified timestamps"
469            }
470            numeric::RangeKind::GteLte => "must be between specified timestamps inclusive",
471            numeric::RangeKind::GteLteExclusive => {
472                "must be greater than or equal to or less than or equal to specified timestamps"
473            }
474            numeric::RangeKind::Gt => "must be greater than specified timestamp",
475            numeric::RangeKind::Gte => "must be greater than or equal to specified timestamp",
476            numeric::RangeKind::Lt => "must be less than specified timestamp",
477            numeric::RangeKind::Lte => "must be less than or equal to specified timestamp",
478        }
479    }
480
481    /// Resolve the range rule for `(seconds, nanos)` bounds with timestamp's
482    /// constant messages. Timestamp ordering is lexicographic on the tuple,
483    /// so [`numeric::RangeKind::violated`] applies to the tuples directly.
484    #[must_use]
485    pub fn range_rule(
486        gt: Option<(i64, i32)>,
487        gte: Option<(i64, i32)>,
488        lt: Option<(i64, i32)>,
489        lte: Option<(i64, i32)>,
490    ) -> Option<numeric::RangeRule> {
491        let mut rule = numeric::range_rule("timestamp", gt, gte, lt, lte, |_| String::new())?;
492        rule.message = range_message(rule.kind).to_string();
493        Some(rule)
494    }
495
496    #[cfg(test)]
497    mod tests {
498        use super::range_rule;
499
500        #[test]
501        fn range_rule_uses_constant_messages() {
502            let rule = range_rule(None, Some((100, 0)), None, Some((200, 0))).expect("bounds");
503            assert_eq!(rule.rule_id, "timestamp.gte_lte");
504            assert_eq!(rule.rule_path, "timestamp.gte");
505            assert_eq!(
506                rule.message,
507                "must be between specified timestamps inclusive"
508            );
509
510            let single = range_rule(Some((5, 0)), None, None, None).expect("bound");
511            assert_eq!(single.rule_id, "timestamp.gt");
512            assert_eq!(single.message, "must be greater than specified timestamp");
513        }
514    }
515}
516
517/// Metadata for string rules.
518pub mod string {
519    /// Rule id for `string.const`.
520    pub const CONST_ID: &str = "string.const";
521    /// Rule id for `string.len`.
522    pub const LEN_ID: &str = "string.len";
523    /// Rule id for `string.min_len`.
524    pub const MIN_LEN_ID: &str = "string.min_len";
525    /// Rule id for `string.max_len`.
526    pub const MAX_LEN_ID: &str = "string.max_len";
527    /// Rule id for `string.len_bytes`.
528    pub const LEN_BYTES_ID: &str = "string.len_bytes";
529    /// Rule id for `string.min_bytes`.
530    pub const MIN_BYTES_ID: &str = "string.min_bytes";
531    /// Rule id for `string.max_bytes`.
532    pub const MAX_BYTES_ID: &str = "string.max_bytes";
533    /// Rule id for `string.pattern`.
534    pub const PATTERN_ID: &str = "string.pattern";
535    /// Rule id for `string.prefix`.
536    pub const PREFIX_ID: &str = "string.prefix";
537    /// Rule id for `string.suffix`.
538    pub const SUFFIX_ID: &str = "string.suffix";
539    /// Rule id for `string.contains`.
540    pub const CONTAINS_ID: &str = "string.contains";
541    /// Rule id for `string.not_contains`.
542    pub const NOT_CONTAINS_ID: &str = "string.not_contains";
543    /// Rule id for `string.in`.
544    pub const IN_ID: &str = "string.in";
545    /// Rule id for `string.not_in`.
546    pub const NOT_IN_ID: &str = "string.not_in";
547
548    /// Rule path shared by both HTTP-header well-known checks.
549    pub const WELL_KNOWN_REGEX_PATH: &str = "string.well_known_regex";
550    /// Rule id for an invalid HTTP header name.
551    pub const HEADER_NAME_ID: &str = "string.well_known_regex.header_name";
552    /// Rule id for an empty HTTP header name.
553    pub const HEADER_NAME_EMPTY_ID: &str = "string.well_known_regex.header_name_empty";
554    /// Rule id for an invalid HTTP header value (no empty variant exists).
555    pub const HEADER_VALUE_ID: &str = "string.well_known_regex.header_value";
556
557    /// The violation message for `string.const`.
558    #[must_use]
559    pub fn const_message(want: &str) -> String {
560        format!("must equal `{want}`")
561    }
562
563    /// The violation message for `string.len`.
564    #[must_use]
565    pub fn len_message(len: u64) -> String {
566        format!("must be {len} characters")
567    }
568
569    /// The violation message for `string.min_len`.
570    #[must_use]
571    pub fn min_len_message(min: u64) -> String {
572        format!("value length must be at least {min} characters")
573    }
574
575    /// The violation message for `string.max_len`.
576    #[must_use]
577    pub fn max_len_message(max: u64) -> String {
578        format!("value length must be at most {max} characters")
579    }
580
581    /// The violation message for `string.len_bytes`.
582    #[must_use]
583    pub fn len_bytes_message(len: u64) -> String {
584        format!("must be {len} bytes")
585    }
586
587    /// The violation message for `string.min_bytes`.
588    #[must_use]
589    pub fn min_bytes_message(min: u64) -> String {
590        format!("must be at least {min} bytes")
591    }
592
593    /// The violation message for `string.max_bytes`.
594    #[must_use]
595    pub fn max_bytes_message(max: u64) -> String {
596        format!("must be at most {max} bytes")
597    }
598
599    /// The violation message for `string.pattern`.
600    #[must_use]
601    pub fn pattern_message(pattern: &str) -> String {
602        format!("does not match regex pattern `{pattern}`")
603    }
604
605    /// The violation message for `string.prefix`.
606    #[must_use]
607    pub fn prefix_message(prefix: &str) -> String {
608        format!("does not have prefix `{prefix}`")
609    }
610
611    /// The violation message for `string.suffix`.
612    #[must_use]
613    pub fn suffix_message(suffix: &str) -> String {
614        format!("does not have suffix `{suffix}`")
615    }
616
617    /// The violation message for `string.contains`.
618    #[must_use]
619    pub fn contains_message(substring: &str) -> String {
620        format!("does not contain substring `{substring}`")
621    }
622
623    /// The violation message for `string.not_contains`.
624    #[must_use]
625    pub fn not_contains_message(substring: &str) -> String {
626        format!("contains substring `{substring}`")
627    }
628
629    /// The violation message for `string.in`. Items are sorted so the
630    /// rendering is deterministic regardless of source order.
631    #[must_use]
632    pub fn in_message(items: &[String]) -> String {
633        format!("must be in list {:?}", sorted(items))
634    }
635
636    /// The violation message for `string.not_in`.
637    #[must_use]
638    pub fn not_in_message(items: &[String]) -> String {
639        format!("must not be in list {:?}", sorted(items))
640    }
641
642    fn sorted(items: &[String]) -> Vec<&String> {
643        let mut v: Vec<&String> = items.iter().collect();
644        v.sort();
645        v
646    }
647
648    /// Rule id (and rule path) for a well-known format, e.g. `string.email`.
649    #[must_use]
650    pub fn well_known_id(name: &str) -> String {
651        format!("string.{name}")
652    }
653
654    /// Rule id for an empty value against a well-known format,
655    /// e.g. `string.email_empty`.
656    #[must_use]
657    pub fn well_known_empty_id(name: &str) -> String {
658        format!("string.{name}_empty")
659    }
660
661    #[cfg(test)]
662    mod tests {
663        use super::{in_message, well_known_empty_id, well_known_id};
664
665        #[test]
666        fn list_messages_sort_and_debug_format() {
667            let items = vec!["b".to_string(), "a".to_string()];
668            assert_eq!(in_message(&items), r#"must be in list ["a", "b"]"#);
669        }
670
671        #[test]
672        fn well_known_ids_compose() {
673            assert_eq!(well_known_id("uri_ref"), "string.uri_ref");
674            assert_eq!(
675                well_known_empty_id("ip_with_prefixlen"),
676                "string.ip_with_prefixlen_empty"
677            );
678        }
679    }
680}
681
682/// Metadata for bytes rules.
683pub mod bytes {
684    /// Rule id for `bytes.const`.
685    pub const CONST_ID: &str = "bytes.const";
686    /// Rule id for `bytes.len`.
687    pub const LEN_ID: &str = "bytes.len";
688    /// Rule id for `bytes.min_len`.
689    pub const MIN_LEN_ID: &str = "bytes.min_len";
690    /// Rule id for `bytes.max_len`.
691    pub const MAX_LEN_ID: &str = "bytes.max_len";
692    /// Rule id for `bytes.pattern`.
693    pub const PATTERN_ID: &str = "bytes.pattern";
694    /// Rule id for `bytes.prefix`.
695    pub const PREFIX_ID: &str = "bytes.prefix";
696    /// Rule id for `bytes.suffix`.
697    pub const SUFFIX_ID: &str = "bytes.suffix";
698    /// Rule id for `bytes.contains`.
699    pub const CONTAINS_ID: &str = "bytes.contains";
700    /// Rule id for `bytes.in`.
701    pub const IN_ID: &str = "bytes.in";
702    /// Rule id for `bytes.not_in`.
703    pub const NOT_IN_ID: &str = "bytes.not_in";
704
705    /// The violation message for `bytes.in`.
706    pub const IN_MESSAGE: &str = "value must be in list";
707    /// The violation message for `bytes.not_in`.
708    pub const NOT_IN_MESSAGE: &str = "value must not be in list";
709
710    /// Rule id for an empty value against `bytes.ip`.
711    pub const IP_EMPTY_ID: &str = "bytes.ip_empty";
712    /// The violation message for an empty value against `bytes.ip`.
713    pub const IP_EMPTY_MESSAGE: &str = "value is empty, which is not a valid IP address";
714    /// Rule id for `bytes.ip`.
715    pub const IP_ID: &str = "bytes.ip";
716    /// The violation message for `bytes.ip`.
717    pub const IP_MESSAGE: &str = "value must be a valid IP address";
718    /// Rule id for an empty value against `bytes.ipv4`.
719    pub const IPV4_EMPTY_ID: &str = "bytes.ipv4_empty";
720    /// The violation message for an empty value against `bytes.ipv4`.
721    pub const IPV4_EMPTY_MESSAGE: &str = "value is empty, which is not a valid IPv4 address";
722    /// Rule id for `bytes.ipv4`.
723    pub const IPV4_ID: &str = "bytes.ipv4";
724    /// The violation message for `bytes.ipv4`.
725    pub const IPV4_MESSAGE: &str = "value must be a valid IPv4 address";
726    /// Rule id for an empty value against `bytes.ipv6`.
727    pub const IPV6_EMPTY_ID: &str = "bytes.ipv6_empty";
728    /// The violation message for an empty value against `bytes.ipv6`.
729    pub const IPV6_EMPTY_MESSAGE: &str = "value is empty, which is not a valid IPv6 address";
730    /// Rule id for `bytes.ipv6`.
731    pub const IPV6_ID: &str = "bytes.ipv6";
732    /// The violation message for `bytes.ipv6`.
733    pub const IPV6_MESSAGE: &str = "value must be a valid IPv6 address";
734    /// Rule id for an empty value against `bytes.uuid`. Unlike the other
735    /// bytes formats this violation carries no message and uses `bytes.uuid`
736    /// as its rule path, matching the conformance corpus.
737    pub const UUID_EMPTY_ID: &str = "bytes.uuid_empty";
738    /// Rule id for `bytes.uuid`; also the rule path of the empty variant.
739    pub const UUID_ID: &str = "bytes.uuid";
740    /// The violation message for `bytes.uuid`.
741    pub const UUID_MESSAGE: &str = "value must be a valid UUID";
742
743    /// The violation message for `bytes.const` (`Vec<u8>` Debug rendering,
744    /// e.g. `[1, 2, 3]`).
745    #[must_use]
746    pub fn const_message(value: &[u8]) -> String {
747        format!("value must be {value:?}")
748    }
749
750    /// The violation message for `bytes.len`.
751    #[must_use]
752    pub fn len_message(len: u64) -> String {
753        format!("value length must be {len} bytes")
754    }
755
756    /// The violation message for `bytes.min_len`.
757    #[must_use]
758    pub fn min_len_message(min: u64) -> String {
759        format!("value length must be at least {min} bytes")
760    }
761
762    /// The violation message for `bytes.max_len`.
763    #[must_use]
764    pub fn max_len_message(max: u64) -> String {
765        format!("value length must be at most {max} bytes")
766    }
767
768    /// The violation message for `bytes.pattern`.
769    #[must_use]
770    pub fn pattern_message(pattern: &str) -> String {
771        format!("value must match regex pattern `{pattern}`")
772    }
773
774    /// The violation message for `bytes.prefix`.
775    #[must_use]
776    pub fn prefix_message(prefix: &[u8]) -> String {
777        format!("value does not have prefix {prefix:?}")
778    }
779
780    /// The violation message for `bytes.suffix`.
781    #[must_use]
782    pub fn suffix_message(suffix: &[u8]) -> String {
783        format!("value does not have suffix {suffix:?}")
784    }
785
786    /// The violation message for `bytes.contains`.
787    #[must_use]
788    pub fn contains_message(substring: &[u8]) -> String {
789        format!("value does not contain {substring:?}")
790    }
791
792    #[cfg(test)]
793    mod tests {
794        use super::const_message;
795
796        #[test]
797        fn byte_messages_use_debug_rendering() {
798            assert_eq!(const_message(&[1, 2, 255]), "value must be [1, 2, 255]");
799        }
800    }
801}
802
803/// Metadata for repeated rules.
804pub mod repeated {
805    /// Rule id for `repeated.min_items`.
806    pub const MIN_ITEMS_ID: &str = "repeated.min_items";
807    /// Rule id for `repeated.max_items`.
808    pub const MAX_ITEMS_ID: &str = "repeated.max_items";
809    /// Rule id for `repeated.unique`.
810    pub const UNIQUE_ID: &str = "repeated.unique";
811    /// The violation message for `repeated.unique`.
812    pub const UNIQUE_MESSAGE: &str = "items must be unique";
813    /// Rule-path prefix prepended to per-item violations.
814    pub const ITEMS_RULE_PREFIX: &str = "repeated.items";
815
816    /// The violation message for `repeated.min_items`.
817    #[must_use]
818    pub fn min_items_message(min: u64) -> String {
819        format!("must have at least {min} items")
820    }
821
822    /// The violation message for `repeated.max_items`.
823    #[must_use]
824    pub fn max_items_message(max: u64) -> String {
825        format!("must have at most {max} items")
826    }
827}
828
829/// Metadata for map rules.
830pub mod map {
831    /// Rule id for `map.min_pairs`.
832    pub const MIN_PAIRS_ID: &str = "map.min_pairs";
833    /// Rule id for `map.max_pairs`.
834    pub const MAX_PAIRS_ID: &str = "map.max_pairs";
835    /// Rule-path prefix prepended to per-key violations.
836    pub const KEYS_RULE_PREFIX: &str = "map.keys";
837    /// Rule-path prefix prepended to per-value violations.
838    pub const VALUES_RULE_PREFIX: &str = "map.values";
839
840    /// The violation message for `map.min_pairs`.
841    #[must_use]
842    pub fn min_pairs_message(min: u64) -> String {
843        format!("must have at least {min} entries")
844    }
845
846    /// The violation message for `map.max_pairs`.
847    #[must_use]
848    pub fn max_pairs_message(max: u64) -> String {
849        format!("must have at most {max} entries")
850    }
851}
852
853/// Metadata for enum rules.
854pub mod enumeration {
855    /// Rule id for `enum.const`.
856    pub const CONST_ID: &str = "enum.const";
857    /// Rule id for `enum.defined_only`.
858    pub const DEFINED_ONLY_ID: &str = "enum.defined_only";
859    /// The violation message for `enum.defined_only`.
860    pub const DEFINED_ONLY_MESSAGE: &str = "value must be one of the defined enum values";
861    /// Rule id for `enum.in`.
862    pub const IN_ID: &str = "enum.in";
863    /// Rule id for `enum.not_in`.
864    pub const NOT_IN_ID: &str = "enum.not_in";
865
866    /// The violation message for `enum.const`.
867    #[must_use]
868    pub fn const_message(value: i32) -> String {
869        format!("must equal {value}")
870    }
871
872    /// The violation message for `enum.in`. Values are sorted so the
873    /// rendering is deterministic regardless of source order.
874    #[must_use]
875    pub fn in_message(values: &[i32]) -> String {
876        format!("must be in list {:?}", sorted(values))
877    }
878
879    /// The violation message for `enum.not_in`.
880    #[must_use]
881    pub fn not_in_message(values: &[i32]) -> String {
882        format!("must not be in list {:?}", sorted(values))
883    }
884
885    fn sorted(values: &[i32]) -> Vec<i32> {
886        let mut v = values.to_vec();
887        v.sort_unstable();
888        v
889    }
890}
891
892/// Metadata for `google.protobuf.FieldMask` rules.
893pub mod field_mask {
894    /// Rule id for `field_mask.const`.
895    pub const CONST_ID: &str = "field_mask.const";
896    /// The violation message for `field_mask.const`.
897    pub const CONST_MESSAGE: &str = "must equal paths";
898    /// Rule id for `field_mask.in`.
899    pub const IN_ID: &str = "field_mask.in";
900    /// The violation message for `field_mask.in`.
901    pub const IN_MESSAGE: &str = "must only contain allowed paths";
902    /// Rule id for `field_mask.not_in`.
903    pub const NOT_IN_ID: &str = "field_mask.not_in";
904    /// The violation message for `field_mask.not_in`.
905    pub const NOT_IN_MESSAGE: &str = "must not contain forbidden paths";
906}
907
908/// Metadata for bool rules.
909pub mod boolean {
910    /// Rule id for `bool.const`.
911    pub const CONST_ID: &str = "bool.const";
912
913    /// The violation message for `bool.const`.
914    #[must_use]
915    pub fn const_message(value: bool) -> String {
916        format!("must equal {value}")
917    }
918}
919
920/// Metadata for `google.protobuf.Any` rules.
921pub mod any {
922    /// Rule id for `any.in`.
923    pub const IN_ID: &str = "any.in";
924    /// The violation message for `any.in`.
925    pub const IN_MESSAGE: &str = "type URL must be in the allow list";
926    /// Rule id for `any.not_in`.
927    pub const NOT_IN_ID: &str = "any.not_in";
928    /// The violation message for `any.not_in`.
929    pub const NOT_IN_MESSAGE: &str = "type URL must not be in the block list";
930}
931
932/// Canonical IEEE-754 handling shared by `repeated.unique` on float/double
933/// fields, in both the runtime evaluator and generated validators.
934pub mod float {
935    /// The uniqueness key for an `f32`: `None` for NaN (NaN never equals
936    /// itself, so multiple NaNs do not violate uniqueness); `+0.0` and
937    /// `-0.0` collapse to the same bit pattern.
938    #[must_use]
939    pub fn canonical_f32_bits(value: f32) -> Option<u32> {
940        if value.is_nan() {
941            return None;
942        }
943        Some(if value == 0.0 {
944            0.0_f32.to_bits()
945        } else {
946            value.to_bits()
947        })
948    }
949
950    /// The uniqueness key for an `f64`; see [`canonical_f32_bits`].
951    #[must_use]
952    pub fn canonical_f64_bits(value: f64) -> Option<u64> {
953        if value.is_nan() {
954            return None;
955        }
956        Some(if value == 0.0 {
957            0.0_f64.to_bits()
958        } else {
959            value.to_bits()
960        })
961    }
962
963    #[cfg(test)]
964    mod tests {
965        use super::{canonical_f32_bits, canonical_f64_bits};
966
967        #[test]
968        fn nan_has_no_key_and_zeros_collapse() {
969            assert_eq!(canonical_f32_bits(f32::NAN), None);
970            assert_eq!(canonical_f64_bits(f64::NAN), None);
971            assert_eq!(canonical_f32_bits(0.0), canonical_f32_bits(-0.0));
972            assert_eq!(canonical_f64_bits(0.0), canonical_f64_bits(-0.0));
973            assert_ne!(canonical_f32_bits(1.0), canonical_f32_bits(2.0));
974        }
975    }
976}