Skip to main content

prost_protovalidate/
violation.rs

1use std::fmt;
2use std::sync::LazyLock;
3
4use prost_reflect::{FieldDescriptor, Kind, MessageDescriptor, Value};
5
6use prost_protovalidate_types::{FieldPath, FieldPathElement, field_path_element};
7
8/// Cached `FieldRules` message descriptor for hydrating rule paths.
9static FIELD_RULES_DESCRIPTOR: LazyLock<Option<MessageDescriptor>> = LazyLock::new(|| {
10    prost_protovalidate_types::DESCRIPTOR_POOL.get_message_by_name("buf.validate.FieldRules")
11});
12
13/// A single instance where a validation rule was not met.
14///
15/// # Construction conventions
16///
17/// Violations are built through one of three patterns, matching the upstream
18/// conformance corpus:
19///
20/// - [`Violation::new`] — the common case; sets `rule_path` equal to
21///   `rule_id` and carries a human-readable message.
22/// - [`Violation::new`] followed by the crate-internal `with_rule_path` —
23///   combined range rules (e.g. rule id `int32.gt_lt` with rule path
24///   `int32.gt`), where the id describes the bound combination but the path
25///   points at one concrete rule field.
26/// - [`Violation::new_constraint`] — well-known format rules (e.g. rule id
27///   `string.email_empty` with rule path `string.email`); the message is
28///   intentionally left empty per the conformance spec.
29#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub struct Violation {
32    /// Wire-compatible payload and canonical source for path/id/message state.
33    proto: prost_protovalidate_types::Violation,
34
35    /// The field descriptor for the violated field, if available.
36    field_descriptor: Option<FieldDescriptor>,
37
38    /// The field value that failed validation, when available.
39    field_value: Option<Value>,
40
41    /// The descriptor for the violated rule field, when available.
42    rule_descriptor: Option<FieldDescriptor>,
43
44    /// The value of the violated rule field, when available.
45    rule_value: Option<Value>,
46
47    /// Extension field path element for predefined rules, preserved across `sync_proto` calls.
48    extension_element: Option<FieldPathElement>,
49}
50
51impl Violation {
52    /// Create a violation with the given field path, rule identifier, and message.
53    ///
54    /// The `rule_path` is set equal to `rule_id`. Enrichment fields
55    /// (`field_descriptor`, `field_value`, `rule_descriptor`, `rule_value`) are
56    /// left as `None` — they are populated only by the runtime validator.
57    pub fn new(
58        field_path: impl Into<String>,
59        rule_id: impl Into<String>,
60        message: impl Into<String>,
61    ) -> Self {
62        let mut out = Self {
63            proto: prost_protovalidate_types::Violation::default(),
64            field_descriptor: None,
65            field_value: None,
66            rule_descriptor: None,
67            rule_value: None,
68            extension_element: None,
69        };
70        out.set_field_path(field_path);
71        let rule_id = rule_id.into();
72        out.set_rule_path(rule_id.clone());
73        out.set_rule_id(rule_id);
74        out.set_message(message);
75        out
76    }
77
78    /// Create a violation for a standard constraint where `rule_path` (the proto
79    /// field path, e.g. `"string.email"`) may differ from `rule_id` (the
80    /// constraint identifier, e.g. `"string.email_empty"`).
81    ///
82    /// The `message` field is intentionally left empty per the conformance spec.
83    /// Enrichment fields (`field_descriptor`, `field_value`, `rule_descriptor`,
84    /// `rule_value`) are left as `None`.
85    pub fn new_constraint(
86        field_path: impl Into<String>,
87        rule_id: impl Into<String>,
88        rule_path: impl Into<String>,
89    ) -> Self {
90        let mut out = Self {
91            proto: prost_protovalidate_types::Violation::default(),
92            field_descriptor: None,
93            field_value: None,
94            rule_descriptor: None,
95            rule_value: None,
96            extension_element: None,
97        };
98        out.set_field_path(field_path);
99        out.set_rule_path(rule_path);
100        out.set_rule_id(rule_id);
101        out
102    }
103
104    /// Serialize this violation into the wire-compatible protobuf message.
105    #[must_use]
106    pub fn to_proto(&self) -> prost_protovalidate_types::Violation {
107        let mut proto = self.proto.clone();
108        hydrate_and_patch_rule_path(&mut proto.rule, self.extension_element.as_ref());
109        proto
110    }
111
112    /// Returns the dot-separated field path where this violation occurred.
113    #[must_use]
114    pub fn field_path(&self) -> String {
115        field_path_string(self.proto.field.as_ref())
116    }
117
118    /// Returns the dot-separated rule path that was violated.
119    #[must_use]
120    pub fn rule_path(&self) -> String {
121        field_path_string(self.proto.rule.as_ref())
122    }
123
124    /// Returns the machine-readable constraint identifier.
125    #[must_use]
126    pub fn rule_id(&self) -> &str {
127        self.proto.rule_id.as_deref().unwrap_or("")
128    }
129
130    /// Returns the human-readable violation message.
131    #[must_use]
132    pub fn message(&self) -> &str {
133        self.proto.message.as_deref().unwrap_or("")
134    }
135
136    /// Returns the field descriptor for the violated field, if available.
137    #[must_use]
138    pub fn field_descriptor(&self) -> Option<&FieldDescriptor> {
139        self.field_descriptor.as_ref()
140    }
141
142    /// Returns the field value that failed validation, when available.
143    #[must_use]
144    pub fn field_value(&self) -> Option<&Value> {
145        self.field_value.as_ref()
146    }
147
148    /// Returns the descriptor for the violated rule field, when available.
149    #[must_use]
150    pub fn rule_descriptor(&self) -> Option<&FieldDescriptor> {
151        self.rule_descriptor.as_ref()
152    }
153
154    /// Returns the value of the violated rule field, when available.
155    #[must_use]
156    pub fn rule_value(&self) -> Option<&Value> {
157        self.rule_value.as_ref()
158    }
159
160    /// Sets the field path.
161    pub fn set_field_path(&mut self, field_path: impl Into<String>) {
162        self.proto.field = parse_path(&field_path.into());
163        if let Some(descriptor) = self.field_descriptor.as_ref() {
164            apply_field_descriptor_to_path(&mut self.proto.field, descriptor);
165        }
166    }
167
168    /// Sets the rule path.
169    pub fn set_rule_path(&mut self, rule_path: impl Into<String>) {
170        self.proto.rule = parse_path(&rule_path.into());
171        hydrate_and_patch_rule_path(&mut self.proto.rule, self.extension_element.as_ref());
172    }
173
174    /// Sets the machine-readable rule identifier.
175    pub fn set_rule_id(&mut self, rule_id: impl Into<String>) {
176        let rule_id = rule_id.into();
177        self.proto.rule_id = if rule_id.is_empty() {
178            None
179        } else {
180            Some(rule_id)
181        };
182    }
183
184    /// Sets the human-readable violation message.
185    pub fn set_message(&mut self, message: impl Into<String>) {
186        let message = message.into();
187        self.proto.message = if message.is_empty() {
188            None
189        } else {
190            Some(message)
191        };
192    }
193
194    pub(crate) fn has_field_descriptor(&self) -> bool {
195        self.field_descriptor.is_some()
196    }
197
198    pub(crate) fn has_field_value(&self) -> bool {
199        self.field_value.is_some()
200    }
201
202    pub(crate) fn has_rule_descriptor(&self) -> bool {
203        self.rule_descriptor.is_some()
204    }
205
206    pub(crate) fn has_rule_value(&self) -> bool {
207        self.rule_value.is_some()
208    }
209
210    pub(crate) fn set_field_descriptor(&mut self, desc: &FieldDescriptor) {
211        self.field_descriptor = Some(desc.clone());
212        apply_field_descriptor_to_path(&mut self.proto.field, desc);
213    }
214
215    pub(crate) fn with_field_descriptor(mut self, desc: &FieldDescriptor) -> Self {
216        self.set_field_descriptor(desc);
217        self
218    }
219
220    pub(crate) fn set_field_value(&mut self, value: Value) {
221        self.field_value = Some(value);
222    }
223
224    pub(crate) fn with_rule_path(mut self, rule_path: impl Into<String>) -> Self {
225        self.set_rule_path(rule_path);
226        self
227    }
228
229    pub(crate) fn set_rule_descriptor(&mut self, descriptor: FieldDescriptor) {
230        self.rule_descriptor = Some(descriptor);
231    }
232
233    pub(crate) fn with_rule_descriptor(mut self, descriptor: FieldDescriptor) -> Self {
234        self.set_rule_descriptor(descriptor);
235        self
236    }
237
238    pub(crate) fn set_rule_value(&mut self, value: Value) {
239        self.rule_value = Some(value);
240    }
241
242    pub(crate) fn with_rule_value(mut self, value: Value) -> Self {
243        self.set_rule_value(value);
244        self
245    }
246
247    /// Append an extension element to the rule path.
248    #[cfg(feature = "cel")]
249    pub(crate) fn with_rule_extension_element(mut self, element: FieldPathElement) -> Self {
250        // Store the extension element so rule path hydration can re-apply metadata.
251        self.extension_element = Some(element.clone());
252        // Append the element to the proto path
253        if let Some(path) = self.proto.rule.as_mut() {
254            path.elements.push(element);
255        } else {
256            self.proto.rule = Some(FieldPath {
257                elements: vec![element],
258            });
259        }
260        hydrate_and_patch_rule_path(&mut self.proto.rule, self.extension_element.as_ref());
261        self
262    }
263
264    /// Strip the rule path so `proto.rule` is `None`.
265    ///
266    /// Used for violations where only `rule_id` should be emitted
267    /// (e.g. oneof, message-level CEL).
268    #[must_use]
269    pub fn without_rule_path(mut self) -> Self {
270        self.proto.rule = None;
271        self
272    }
273
274    /// Mark this violation as caused by a map key (rather than a value).
275    ///
276    /// Set by the runtime map evaluator on every key-rule violation and by
277    /// generated validators when iterating map-key constraints — preserves
278    /// the `for_key` field on the wire-level [`Violation`] proto.
279    pub fn mark_for_key(&mut self) {
280        self.proto.for_key = Some(true);
281    }
282
283    /// Returns whether this violation was caused by a map key (rather than a value).
284    ///
285    /// `None` when the field is unset on the wire (the common case for
286    /// non-map-key violations); `Some(true)` after [`Violation::mark_for_key`].
287    #[must_use]
288    pub fn for_key(&self) -> Option<bool> {
289        self.proto.for_key
290    }
291
292    /// Prepend a parent field path element.
293    pub fn prepend_field_path(&mut self, parent: &str) {
294        if parent.is_empty() {
295            return;
296        }
297        prepend_proto_field_path(&mut self.proto.field, parent, None);
298    }
299
300    /// Prepend a parent field path with a `repeated` index subscript:
301    /// `parent[index].<existing>`.
302    pub fn prepend_index(&mut self, parent: &str, index: u64) {
303        if parent.is_empty() {
304            return;
305        }
306        prepend_with_subscript(
307            &mut self.proto.field,
308            parent,
309            field_path_element::Subscript::Index(index),
310        );
311    }
312
313    /// Prepend a parent field path with a string-keyed map subscript:
314    /// `parent["key"].<existing>`. The key is JSON-escaped on rendering,
315    /// matching the canonical runtime format for map paths.
316    pub fn prepend_string_key(&mut self, parent: &str, key: &str) {
317        if parent.is_empty() {
318            return;
319        }
320        prepend_with_subscript(
321            &mut self.proto.field,
322            parent,
323            field_path_element::Subscript::StringKey(key.to_string()),
324        );
325    }
326
327    /// Prepend a parent field path with a signed-integer-keyed map subscript:
328    /// `parent[key].<existing>`.
329    pub fn prepend_int_key(&mut self, parent: &str, key: i64) {
330        if parent.is_empty() {
331            return;
332        }
333        prepend_with_subscript(
334            &mut self.proto.field,
335            parent,
336            field_path_element::Subscript::IntKey(key),
337        );
338    }
339
340    /// Prepend a parent field path with an unsigned-integer-keyed map subscript:
341    /// `parent[key].<existing>`.
342    pub fn prepend_uint_key(&mut self, parent: &str, key: u64) {
343        if parent.is_empty() {
344            return;
345        }
346        prepend_with_subscript(
347            &mut self.proto.field,
348            parent,
349            field_path_element::Subscript::UintKey(key),
350        );
351    }
352
353    /// Prepend a parent field path with a bool-keyed map subscript:
354    /// `parent[true].<existing>` or `parent[false].<existing>`.
355    pub fn prepend_bool_key(&mut self, parent: &str, key: bool) {
356        if parent.is_empty() {
357            return;
358        }
359        prepend_with_subscript(
360            &mut self.proto.field,
361            parent,
362            field_path_element::Subscript::BoolKey(key),
363        );
364    }
365
366    pub(crate) fn prepend_path_with_descriptor(
367        &mut self,
368        parent: &str,
369        descriptor: &FieldDescriptor,
370    ) {
371        if parent.is_empty() {
372            return;
373        }
374        prepend_proto_field_path(&mut self.proto.field, parent, Some(descriptor));
375    }
376
377    /// Prepend a parent rule path element.
378    ///
379    /// Used by generated validators to splice container-rule path
380    /// segments (e.g. `repeated.items`, `map.keys`, `map.values`) onto
381    /// item-level violations so the final `rule_path` matches the
382    /// runtime emission.
383    pub fn prepend_rule_path(&mut self, parent: &str) {
384        if parent.is_empty() {
385            return;
386        }
387        let current = self.rule_path();
388        if current.is_empty() {
389            self.set_rule_path(parent.to_string());
390        } else {
391            self.set_rule_path(format!("{parent}.{current}"));
392        }
393    }
394}
395
396/// Prepend a single field-path element with a subscript before any existing path.
397///
398/// When the existing path begins with a subscript-only element (a bare
399/// `[…]` produced by an inner nested validator), the inner subscript is
400/// merged into the new prefix to avoid leaving an orphan element — same
401/// rule applied by [`prepend_proto_field_path`] for descriptor-based
402/// prepends.
403fn prepend_with_subscript(
404    path: &mut Option<FieldPath>,
405    parent: &str,
406    subscript: field_path_element::Subscript,
407) {
408    let mut prefix_element = FieldPathElement {
409        field_name: Some(parent.to_string()),
410        subscript: Some(subscript),
411        ..FieldPathElement::default()
412    };
413
414    let suffix_elements = match path.take() {
415        Some(existing) => existing.elements,
416        None => Vec::new(),
417    };
418
419    let mut iter = suffix_elements.into_iter();
420    let mut merged = Vec::with_capacity(iter.size_hint().0 + 1);
421
422    if let Some(first) = iter.next() {
423        if is_subscript_only_element(&first) && prefix_element.subscript.is_none() {
424            prefix_element.subscript.clone_from(&first.subscript);
425        } else {
426            merged.push(first);
427        }
428    }
429    merged.insert(0, prefix_element);
430    merged.extend(iter);
431
432    *path = Some(FieldPath { elements: merged });
433}
434
435fn apply_field_descriptor_to_path(path: &mut Option<FieldPath>, desc: &FieldDescriptor) {
436    if let Some(path) = path.as_mut() {
437        if let Some(first) = path.elements.first_mut() {
438            let subscript = normalize_subscript_for_descriptor(first.subscript.take(), desc);
439            *first = field_path_element_from_descriptor(desc);
440            first.subscript = subscript;
441            apply_map_metadata(first, desc);
442        } else {
443            path.elements.push(field_path_element_from_descriptor(desc));
444        }
445    } else {
446        *path = Some(FieldPath {
447            elements: vec![field_path_element_from_descriptor(desc)],
448        });
449    }
450}
451
452fn hydrate_and_patch_rule_path(
453    path: &mut Option<FieldPath>,
454    extension_element: Option<&FieldPathElement>,
455) {
456    hydrate_rule_path(path);
457    // Re-apply stored extension element metadata (field_number, field_type)
458    // that parse_path cannot reconstruct from the string representation.
459    if let (Some(ext), Some(path)) = (extension_element, path.as_mut()) {
460        if let Some(ext_name) = &ext.field_name {
461            for el in &mut path.elements {
462                if el.field_name.as_deref() == Some(ext_name) {
463                    el.field_number = ext.field_number;
464                    el.field_type = ext.field_type;
465                }
466            }
467        }
468    }
469}
470
471fn field_path_element_from_descriptor(desc: &FieldDescriptor) -> FieldPathElement {
472    FieldPathElement {
473        field_number: i32::try_from(desc.number()).ok(),
474        field_name: Some(desc.name().to_string()),
475        field_type: Some(if desc.is_group() {
476            prost_types::field_descriptor_proto::Type::Group
477        } else {
478            kind_to_descriptor_type(&desc.kind())
479        } as i32),
480        key_type: None,
481        value_type: None,
482        subscript: None,
483    }
484}
485
486/// Populate `key_type` / `value_type` on an element when it has a subscript
487/// and the underlying field is a map.
488fn apply_map_metadata(element: &mut FieldPathElement, desc: &FieldDescriptor) {
489    if desc.is_map() && element.subscript.is_some() {
490        let (key_type, value_type) = map_key_value_types(desc);
491        element.key_type = key_type;
492        element.value_type = value_type;
493    }
494}
495
496/// Extract the key and value field types for a map field descriptor.
497fn map_key_value_types(desc: &FieldDescriptor) -> (Option<i32>, Option<i32>) {
498    let kind = desc.kind();
499    let Some(entry) = kind.as_message() else {
500        return (None, None);
501    };
502    let key_type = entry
503        .get_field_by_name("key")
504        .map(|f| kind_to_descriptor_type(&f.kind()) as i32);
505    let value_type = entry
506        .get_field_by_name("value")
507        .map(|f| kind_to_descriptor_type(&f.kind()) as i32);
508    (key_type, value_type)
509}
510
511fn normalize_subscript_for_descriptor(
512    subscript: Option<field_path_element::Subscript>,
513    desc: &FieldDescriptor,
514) -> Option<field_path_element::Subscript> {
515    let subscript = subscript?;
516
517    if !desc.is_map() {
518        return Some(subscript);
519    }
520
521    let kind = desc.kind();
522    let Some(entry_desc) = kind.as_message() else {
523        return Some(subscript);
524    };
525    let Some(key_field) = entry_desc.get_field_by_name("key") else {
526        return Some(subscript);
527    };
528
529    match (subscript, key_field.kind()) {
530        (
531            field_path_element::Subscript::Index(value),
532            Kind::Int32
533            | Kind::Int64
534            | Kind::Sint32
535            | Kind::Sint64
536            | Kind::Sfixed32
537            | Kind::Sfixed64,
538        ) => i64::try_from(value)
539            .map(field_path_element::Subscript::IntKey)
540            .ok()
541            .or(Some(field_path_element::Subscript::Index(value))),
542        (
543            field_path_element::Subscript::Index(value),
544            Kind::Uint32 | Kind::Uint64 | Kind::Fixed32 | Kind::Fixed64,
545        ) => Some(field_path_element::Subscript::UintKey(value)),
546        (subscript, _) => Some(subscript),
547    }
548}
549
550pub(crate) fn kind_to_descriptor_type(kind: &Kind) -> prost_types::field_descriptor_proto::Type {
551    match *kind {
552        Kind::Double => prost_types::field_descriptor_proto::Type::Double,
553        Kind::Float => prost_types::field_descriptor_proto::Type::Float,
554        Kind::Int64 => prost_types::field_descriptor_proto::Type::Int64,
555        Kind::Uint64 => prost_types::field_descriptor_proto::Type::Uint64,
556        Kind::Int32 => prost_types::field_descriptor_proto::Type::Int32,
557        Kind::Fixed64 => prost_types::field_descriptor_proto::Type::Fixed64,
558        Kind::Fixed32 => prost_types::field_descriptor_proto::Type::Fixed32,
559        Kind::Bool => prost_types::field_descriptor_proto::Type::Bool,
560        Kind::String => prost_types::field_descriptor_proto::Type::String,
561        Kind::Message(_) => prost_types::field_descriptor_proto::Type::Message,
562        Kind::Bytes => prost_types::field_descriptor_proto::Type::Bytes,
563        Kind::Uint32 => prost_types::field_descriptor_proto::Type::Uint32,
564        Kind::Enum(_) => prost_types::field_descriptor_proto::Type::Enum,
565        Kind::Sfixed32 => prost_types::field_descriptor_proto::Type::Sfixed32,
566        Kind::Sfixed64 => prost_types::field_descriptor_proto::Type::Sfixed64,
567        Kind::Sint32 => prost_types::field_descriptor_proto::Type::Sint32,
568        Kind::Sint64 => prost_types::field_descriptor_proto::Type::Sint64,
569    }
570}
571
572fn prepend_proto_field_path(
573    path: &mut Option<FieldPath>,
574    parent: &str,
575    descriptor: Option<&FieldDescriptor>,
576) {
577    let Some(mut prefix) = parse_path(parent) else {
578        return;
579    };
580
581    if let Some(descriptor) = descriptor {
582        if let Some(first) = prefix.elements.first_mut() {
583            let subscript = normalize_subscript_for_descriptor(first.subscript.take(), descriptor);
584            *first = field_path_element_from_descriptor(descriptor);
585            first.subscript = subscript;
586            apply_map_metadata(first, descriptor);
587        } else {
588            prefix
589                .elements
590                .push(field_path_element_from_descriptor(descriptor));
591        }
592    }
593
594    let Some(mut suffix) = path.take() else {
595        *path = Some(prefix);
596        return;
597    };
598
599    if let (Some(last_prefix), Some(first_suffix)) =
600        (prefix.elements.last_mut(), suffix.elements.first())
601    {
602        if is_subscript_only_element(first_suffix) && last_prefix.subscript.is_none() {
603            last_prefix.subscript.clone_from(&first_suffix.subscript);
604            suffix.elements.remove(0);
605            // After merging the subscript, normalize it and populate map metadata.
606            if let Some(descriptor) = descriptor {
607                last_prefix.subscript =
608                    normalize_subscript_for_descriptor(last_prefix.subscript.take(), descriptor);
609                apply_map_metadata(last_prefix, descriptor);
610            }
611        }
612    }
613
614    prefix.elements.extend(suffix.elements);
615    *path = Some(prefix);
616}
617
618fn is_subscript_only_element(element: &FieldPathElement) -> bool {
619    element.field_name.is_none()
620        && element.field_number.is_none()
621        && element.field_type.is_none()
622        && element.key_type.is_none()
623        && element.value_type.is_none()
624        && element.subscript.is_some()
625}
626
627fn parse_path(path: &str) -> Option<FieldPath> {
628    if path.is_empty() {
629        return None;
630    }
631
632    let mut elements = Vec::new();
633    for segment in split_segments(path) {
634        let (name, subscripts) = split_name_and_subscripts(segment);
635
636        // When a segment is entirely a bracketed token that isn't a valid
637        // subscript (e.g. `[buf.validate.conformance.cases.ext_name]`),
638        // split_name_and_subscripts returns ("", []).  Treat the entire
639        // segment as an extension field name.
640        if name.is_empty()
641            && subscripts.is_empty()
642            && segment.starts_with('[')
643            && segment.ends_with(']')
644        {
645            elements.push(FieldPathElement {
646                field_name: Some(segment.to_string()),
647                ..FieldPathElement::default()
648            });
649            continue;
650        }
651
652        if !name.is_empty() || subscripts.is_empty() {
653            elements.push(FieldPathElement {
654                field_name: if name.is_empty() { None } else { Some(name) },
655                ..FieldPathElement::default()
656            });
657        }
658
659        for (idx, subscript) in subscripts.into_iter().enumerate() {
660            if idx == 0 && !elements.is_empty() {
661                if let Some(last) = elements.last_mut() {
662                    last.subscript = Some(subscript);
663                }
664            } else {
665                elements.push(FieldPathElement {
666                    subscript: Some(subscript),
667                    ..FieldPathElement::default()
668                });
669            }
670        }
671    }
672
673    Some(FieldPath { elements })
674}
675
676fn split_segments(path: &str) -> Vec<&str> {
677    let mut segments = Vec::new();
678    let mut start = 0usize;
679    let mut depth = 0usize;
680
681    for (idx, ch) in path.char_indices() {
682        match ch {
683            '[' => depth += 1,
684            ']' => depth = depth.saturating_sub(1),
685            '.' if depth == 0 => {
686                segments.push(&path[start..idx]);
687                start = idx + 1;
688            }
689            _ => {}
690        }
691    }
692
693    if start < path.len() {
694        segments.push(&path[start..]);
695    }
696
697    segments
698}
699
700fn split_name_and_subscripts(segment: &str) -> (String, Vec<field_path_element::Subscript>) {
701    let name_end = segment.find('[').unwrap_or(segment.len());
702    let name = segment[..name_end].to_string();
703    let mut subscripts = Vec::new();
704    let mut rest = &segment[name_end..];
705
706    while let Some(open_idx) = rest.find('[') {
707        let Some(close_rel) = rest[open_idx + 1..].find(']') else {
708            break;
709        };
710        let close_idx = open_idx + 1 + close_rel;
711        let token = &rest[open_idx + 1..close_idx];
712        if let Some(subscript) = parse_subscript(token) {
713            subscripts.push(subscript);
714        }
715        rest = &rest[close_idx + 1..];
716    }
717
718    (name, subscripts)
719}
720
721fn parse_subscript(token: &str) -> Option<field_path_element::Subscript> {
722    if token.starts_with('"') && token.ends_with('"') && token.len() >= 2 {
723        if let Ok(decoded) = serde_json::from_str::<String>(token) {
724            return Some(field_path_element::Subscript::StringKey(decoded));
725        }
726    }
727
728    if token.eq_ignore_ascii_case("true") {
729        return Some(field_path_element::Subscript::BoolKey(true));
730    }
731
732    if token.eq_ignore_ascii_case("false") {
733        return Some(field_path_element::Subscript::BoolKey(false));
734    }
735
736    if let Ok(index) = token.parse::<u64>() {
737        return Some(field_path_element::Subscript::Index(index));
738    }
739
740    if let Ok(int_key) = token.parse::<i64>() {
741        return Some(field_path_element::Subscript::IntKey(int_key));
742    }
743
744    None
745}
746
747/// Resolve each element of a rule [`FieldPath`] against the `FieldRules`
748/// descriptor chain, populating `field_number` and `field_type`.
749fn hydrate_rule_path(path: &mut Option<FieldPath>) {
750    let Some(path) = path.as_mut() else {
751        return;
752    };
753    let Some(mut descriptor) = FIELD_RULES_DESCRIPTOR.clone() else {
754        return;
755    };
756    for element in &mut path.elements {
757        let Some(name) = element.field_name.as_deref() else {
758            continue;
759        };
760        // Extension field names are wrapped in brackets (e.g.
761        // `[buf.validate.conformance.cases.ext]`). They aren't regular
762        // fields so skip hydration — the builder already populated their
763        // field_number and field_type.
764        if name.starts_with('[') {
765            continue;
766        }
767        let Some(field) = descriptor.get_field_by_name(name) else {
768            break;
769        };
770        element.field_number = i32::try_from(field.number()).ok();
771        element.field_type = if field.is_group() {
772            Some(prost_types::field_descriptor_proto::Type::Group as i32)
773        } else {
774            Some(kind_to_descriptor_type(&field.kind()) as i32)
775        };
776        if let Some(msg) = field.kind().as_message() {
777            descriptor = msg.clone();
778        }
779    }
780}
781
782fn field_path_string(path: Option<&FieldPath>) -> String {
783    let Some(path) = path else {
784        return String::new();
785    };
786
787    let mut out = String::new();
788    for element in &path.elements {
789        if let Some(name) = &element.field_name {
790            if !name.is_empty() {
791                // Insert a dot between any two adjacent path components.
792                // The previous component may already end in `]` (a map or
793                // repeated subscript) — the canonical protovalidate format
794                // still places a separator before the next field name:
795                // `items["alpha"].value`, not `items["alpha"]value`.
796                if !out.is_empty() {
797                    out.push('.');
798                }
799                out.push_str(name);
800            }
801        }
802
803        if let Some(subscript) = &element.subscript {
804            out.push('[');
805            match subscript {
806                field_path_element::Subscript::Index(i)
807                | field_path_element::Subscript::UintKey(i) => out.push_str(&i.to_string()),
808                field_path_element::Subscript::BoolKey(b) => out.push_str(&b.to_string()),
809                field_path_element::Subscript::IntKey(i) => out.push_str(&i.to_string()),
810                field_path_element::Subscript::StringKey(s) => {
811                    let encoded = serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string());
812                    out.push_str(&encoded);
813                }
814            }
815            out.push(']');
816        }
817    }
818
819    out
820}
821
822impl fmt::Display for Violation {
823    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
824        let has_path = self
825            .proto
826            .field
827            .as_ref()
828            .is_some_and(|p| !p.elements.is_empty());
829
830        if has_path {
831            write!(f, "{}: ", self.field_path())?;
832        }
833        if !self.message().is_empty() {
834            write!(f, "{}", self.message())
835        } else if !self.rule_id().is_empty() {
836            write!(f, "[{}]", self.rule_id())
837        } else {
838            write!(f, "[unknown]")
839        }
840    }
841}
842
843#[cfg(test)]
844mod tests {
845    use std::fmt::Write;
846
847    use pretty_assertions::assert_eq;
848    use proptest::collection::vec;
849    use proptest::prelude::*;
850
851    use super::{Violation, field_path_string, parse_path};
852
853    fn descriptor_field(message: &str, field: &str) -> prost_reflect::FieldDescriptor {
854        prost_protovalidate_types::DESCRIPTOR_POOL
855            .get_message_by_name(message)
856            .and_then(|message| message.get_field_by_name(field))
857            .expect("descriptor field must exist")
858    }
859
860    #[test]
861    fn prepend_path_with_descriptor_preserves_nested_descriptor_metadata() {
862        let parent = descriptor_field("buf.validate.FieldRules", "string");
863        let child = descriptor_field("buf.validate.StringRules", "min_len");
864
865        let mut violation = Violation::new("min_len", "string.min_len", "must be >= 1")
866            .with_field_descriptor(&child);
867        violation.prepend_path_with_descriptor("string", &parent);
868
869        let path = violation
870            .proto
871            .field
872            .as_ref()
873            .expect("field path should be populated");
874        assert_eq!(path.elements.len(), 2);
875
876        let parent_element = &path.elements[0];
877        assert_eq!(parent_element.field_name.as_deref(), Some("string"));
878        assert_eq!(
879            parent_element.field_number,
880            i32::try_from(parent.number()).ok()
881        );
882
883        let child_element = &path.elements[1];
884        assert_eq!(child_element.field_name.as_deref(), Some("min_len"));
885        assert_eq!(
886            child_element.field_number,
887            i32::try_from(child.number()).ok()
888        );
889    }
890
891    #[test]
892    fn field_path_string_round_trips_json_escaped_subscripts() {
893        let raw = "line\n\t\"quote\"\\slash";
894        let encoded = serde_json::to_string(raw).expect("json encoding should succeed");
895        let mut violation = Violation::new(format!("[{encoded}]"), "string.min_len", "bad");
896        violation.prepend_field_path("rules");
897
898        let rendered = field_path_string(violation.proto.field.as_ref());
899        assert_eq!(rendered, format!("rules[{encoded}]"));
900    }
901
902    #[test]
903    fn field_path_string_uses_proper_json_escaping_for_map_keys() {
904        let raw = "line\nvalue";
905        let encoded = serde_json::to_string(raw).expect("json encoding should succeed");
906        let violation = Violation::new(
907            format!("pattern[{encoded}]"),
908            "string.pattern",
909            "must match pattern",
910        );
911        assert_eq!(
912            field_path_string(violation.proto.field.as_ref()),
913            format!("pattern[{encoded}]")
914        );
915    }
916
917    #[test]
918    fn field_path_string_inserts_dot_after_map_subscript() {
919        // Canonical protovalidate format places a `.` between a map
920        // subscript and the next field name: `items["alpha"].value`,
921        // not `items["alpha"]value`. Regression guard for the
922        // `field_path_string` renderer.
923        let mut violation = Violation::new("value", "string.min_len", "must be >= 1");
924        violation.prepend_string_key("items", "alpha");
925
926        assert_eq!(
927            field_path_string(violation.proto.field.as_ref()),
928            "items[\"alpha\"].value",
929        );
930    }
931
932    #[test]
933    fn field_path_string_inserts_dot_after_repeated_subscript() {
934        // Same rule for repeated subscripts: `xs[0].name`, not `xs[0]name`.
935        let mut violation = Violation::new("name", "string.min_len", "must be >= 1");
936        violation.prepend_index("xs", 0);
937
938        assert_eq!(
939            field_path_string(violation.proto.field.as_ref()),
940            "xs[0].name",
941        );
942    }
943
944    #[test]
945    fn violation_display_prefers_field_and_message_then_rule_id_then_unknown() {
946        let with_path_and_message = Violation::new("one.two", "bar", "foo");
947        assert_eq!(with_path_and_message.to_string(), "one.two: foo");
948
949        let message_only = Violation::new("", "bar", "foo");
950        assert_eq!(message_only.to_string(), "foo");
951
952        let rule_id_only = Violation::new("", "bar", "");
953        assert_eq!(rule_id_only.to_string(), "[bar]");
954
955        let unknown = Violation::new("", "", "");
956        assert_eq!(unknown.to_string(), "[unknown]");
957    }
958
959    #[test]
960    fn hydrate_rule_path_populates_field_number_and_type() {
961        let violation = Violation::new("val", "int32.const", "must equal 1");
962        let rule = violation
963            .proto
964            .rule
965            .as_ref()
966            .expect("rule path should be populated");
967
968        assert_eq!(rule.elements.len(), 2);
969
970        let first = &rule.elements[0];
971        assert_eq!(first.field_name.as_deref(), Some("int32"));
972        assert!(
973            first.field_number.is_some(),
974            "int32 element must have field_number"
975        );
976        assert!(
977            first.field_type.is_some(),
978            "int32 element must have field_type"
979        );
980
981        let second = &rule.elements[1];
982        assert_eq!(second.field_name.as_deref(), Some("const"));
983        assert!(
984            second.field_number.is_some(),
985            "const element must have field_number"
986        );
987        assert!(
988            second.field_type.is_some(),
989            "const element must have field_type"
990        );
991    }
992
993    #[test]
994    fn hydrate_rule_path_handles_unknown_names_gracefully() {
995        let violation = Violation::new("val", "nonexistent.field", "message");
996        let rule = violation
997            .proto
998            .rule
999            .as_ref()
1000            .expect("rule path should be populated");
1001
1002        // First element is unknown, so it should NOT be hydrated
1003        let first = &rule.elements[0];
1004        assert_eq!(first.field_name.as_deref(), Some("nonexistent"));
1005        assert_eq!(first.field_number, None);
1006    }
1007
1008    proptest! {
1009        #[test]
1010        fn dotted_paths_round_trip_through_parser(
1011            segments in vec("[a-zA-Z_][a-zA-Z0-9_]{0,8}", 1..6)
1012        ) {
1013            let path = segments.join(".");
1014            let parsed = parse_path(&path);
1015            prop_assert_eq!(field_path_string(parsed.as_ref()), path);
1016        }
1017
1018        #[test]
1019        fn indexed_paths_round_trip_through_parser(
1020            name in "[a-zA-Z_][a-zA-Z0-9_]{0,8}",
1021            indexes in vec(0_u16..1000, 1..4)
1022        ) {
1023            let mut path = name;
1024            for index in &indexes {
1025                let _ = write!(path, "[{index}]");
1026            }
1027            let parsed = parse_path(&path);
1028            prop_assert_eq!(field_path_string(parsed.as_ref()), path);
1029        }
1030    }
1031}