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