Skip to main content

prost_protovalidate/
violation.rs

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