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