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
8static FIELD_RULES_DESCRIPTOR: LazyLock<Option<MessageDescriptor>> = LazyLock::new(|| {
10 prost_protovalidate_types::DESCRIPTOR_POOL.get_message_by_name("buf.validate.FieldRules")
11});
12
13#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub struct Violation {
32 proto: prost_protovalidate_types::Violation,
34
35 field_descriptor: Option<FieldDescriptor>,
37
38 field_value: Option<Value>,
40
41 rule_descriptor: Option<FieldDescriptor>,
43
44 rule_value: Option<Value>,
46
47 extension_element: Option<FieldPathElement>,
49}
50
51impl Violation {
52 pub fn new(
58 field_path: impl Into<String>,
59 rule_id: impl Into<String>,
60 message: impl Into<String>,
61 ) -> Self {
62 let mut out = Self {
63 proto: prost_protovalidate_types::Violation::default(),
64 field_descriptor: None,
65 field_value: None,
66 rule_descriptor: None,
67 rule_value: None,
68 extension_element: None,
69 };
70 out.set_field_path(field_path);
71 let rule_id = rule_id.into();
72 out.set_rule_path(rule_id.clone());
73 out.set_rule_id(rule_id);
74 out.set_message(message);
75 out
76 }
77
78 pub fn new_constraint(
86 field_path: impl Into<String>,
87 rule_id: impl Into<String>,
88 rule_path: impl Into<String>,
89 ) -> Self {
90 let mut out = Self {
91 proto: prost_protovalidate_types::Violation::default(),
92 field_descriptor: None,
93 field_value: None,
94 rule_descriptor: None,
95 rule_value: None,
96 extension_element: None,
97 };
98 out.set_field_path(field_path);
99 out.set_rule_path(rule_path);
100 out.set_rule_id(rule_id);
101 out
102 }
103
104 #[must_use]
106 pub fn to_proto(&self) -> prost_protovalidate_types::Violation {
107 let mut proto = self.proto.clone();
108 hydrate_and_patch_rule_path(&mut proto.rule, self.extension_element.as_ref());
109 proto
110 }
111
112 #[must_use]
114 pub fn field_path(&self) -> String {
115 field_path_string(self.proto.field.as_ref())
116 }
117
118 #[must_use]
120 pub fn rule_path(&self) -> String {
121 field_path_string(self.proto.rule.as_ref())
122 }
123
124 #[must_use]
126 pub fn rule_id(&self) -> &str {
127 self.proto.rule_id.as_deref().unwrap_or("")
128 }
129
130 #[must_use]
132 pub fn message(&self) -> &str {
133 self.proto.message.as_deref().unwrap_or("")
134 }
135
136 #[must_use]
138 pub fn field_descriptor(&self) -> Option<&FieldDescriptor> {
139 self.field_descriptor.as_ref()
140 }
141
142 #[must_use]
144 pub fn field_value(&self) -> Option<&Value> {
145 self.field_value.as_ref()
146 }
147
148 #[must_use]
150 pub fn rule_descriptor(&self) -> Option<&FieldDescriptor> {
151 self.rule_descriptor.as_ref()
152 }
153
154 #[must_use]
156 pub fn rule_value(&self) -> Option<&Value> {
157 self.rule_value.as_ref()
158 }
159
160 pub fn set_field_path(&mut self, field_path: impl Into<String>) {
162 self.proto.field = parse_path(&field_path.into());
163 if let Some(descriptor) = self.field_descriptor.as_ref() {
164 apply_field_descriptor_to_path(&mut self.proto.field, descriptor);
165 }
166 }
167
168 pub fn set_rule_path(&mut self, rule_path: impl Into<String>) {
170 self.proto.rule = parse_path(&rule_path.into());
171 hydrate_and_patch_rule_path(&mut self.proto.rule, self.extension_element.as_ref());
172 }
173
174 pub fn set_rule_id(&mut self, rule_id: impl Into<String>) {
176 let rule_id = rule_id.into();
177 self.proto.rule_id = if rule_id.is_empty() {
178 None
179 } else {
180 Some(rule_id)
181 };
182 }
183
184 pub fn set_message(&mut self, message: impl Into<String>) {
186 let message = message.into();
187 self.proto.message = if message.is_empty() {
188 None
189 } else {
190 Some(message)
191 };
192 }
193
194 pub(crate) fn has_field_descriptor(&self) -> bool {
195 self.field_descriptor.is_some()
196 }
197
198 pub(crate) fn has_field_value(&self) -> bool {
199 self.field_value.is_some()
200 }
201
202 pub(crate) fn has_rule_descriptor(&self) -> bool {
203 self.rule_descriptor.is_some()
204 }
205
206 pub(crate) fn has_rule_value(&self) -> bool {
207 self.rule_value.is_some()
208 }
209
210 pub(crate) fn set_field_descriptor(&mut self, desc: &FieldDescriptor) {
211 self.field_descriptor = Some(desc.clone());
212 apply_field_descriptor_to_path(&mut self.proto.field, desc);
213 }
214
215 pub(crate) fn with_field_descriptor(mut self, desc: &FieldDescriptor) -> Self {
216 self.set_field_descriptor(desc);
217 self
218 }
219
220 pub(crate) fn set_field_value(&mut self, value: Value) {
221 self.field_value = Some(value);
222 }
223
224 pub(crate) fn with_rule_path(mut self, rule_path: impl Into<String>) -> Self {
225 self.set_rule_path(rule_path);
226 self
227 }
228
229 pub(crate) fn set_rule_descriptor(&mut self, descriptor: FieldDescriptor) {
230 self.rule_descriptor = Some(descriptor);
231 }
232
233 pub(crate) fn with_rule_descriptor(mut self, descriptor: FieldDescriptor) -> Self {
234 self.set_rule_descriptor(descriptor);
235 self
236 }
237
238 pub(crate) fn set_rule_value(&mut self, value: Value) {
239 self.rule_value = Some(value);
240 }
241
242 pub(crate) fn with_rule_value(mut self, value: Value) -> Self {
243 self.set_rule_value(value);
244 self
245 }
246
247 #[cfg(feature = "cel")]
249 pub(crate) fn with_rule_extension_element(mut self, element: FieldPathElement) -> Self {
250 self.extension_element = Some(element.clone());
252 if let Some(path) = self.proto.rule.as_mut() {
254 path.elements.push(element);
255 } else {
256 self.proto.rule = Some(FieldPath {
257 elements: vec![element],
258 });
259 }
260 hydrate_and_patch_rule_path(&mut self.proto.rule, self.extension_element.as_ref());
261 self
262 }
263
264 #[must_use]
269 pub fn without_rule_path(mut self) -> Self {
270 self.proto.rule = None;
271 self
272 }
273
274 pub fn mark_for_key(&mut self) {
280 self.proto.for_key = Some(true);
281 }
282
283 #[must_use]
288 pub fn for_key(&self) -> Option<bool> {
289 self.proto.for_key
290 }
291
292 pub fn prepend_field_path(&mut self, parent: &str) {
294 if parent.is_empty() {
295 return;
296 }
297 prepend_proto_field_path(&mut self.proto.field, parent, None);
298 }
299
300 pub fn prepend_index(&mut self, parent: &str, index: u64) {
303 if parent.is_empty() {
304 return;
305 }
306 prepend_with_subscript(
307 &mut self.proto.field,
308 parent,
309 field_path_element::Subscript::Index(index),
310 );
311 }
312
313 pub fn prepend_string_key(&mut self, parent: &str, key: &str) {
317 if parent.is_empty() {
318 return;
319 }
320 prepend_with_subscript(
321 &mut self.proto.field,
322 parent,
323 field_path_element::Subscript::StringKey(key.to_string()),
324 );
325 }
326
327 pub fn prepend_int_key(&mut self, parent: &str, key: i64) {
330 if parent.is_empty() {
331 return;
332 }
333 prepend_with_subscript(
334 &mut self.proto.field,
335 parent,
336 field_path_element::Subscript::IntKey(key),
337 );
338 }
339
340 pub fn prepend_uint_key(&mut self, parent: &str, key: u64) {
343 if parent.is_empty() {
344 return;
345 }
346 prepend_with_subscript(
347 &mut self.proto.field,
348 parent,
349 field_path_element::Subscript::UintKey(key),
350 );
351 }
352
353 pub fn prepend_bool_key(&mut self, parent: &str, key: bool) {
356 if parent.is_empty() {
357 return;
358 }
359 prepend_with_subscript(
360 &mut self.proto.field,
361 parent,
362 field_path_element::Subscript::BoolKey(key),
363 );
364 }
365
366 pub(crate) fn prepend_path_with_descriptor(
367 &mut self,
368 parent: &str,
369 descriptor: &FieldDescriptor,
370 ) {
371 if parent.is_empty() {
372 return;
373 }
374 prepend_proto_field_path(&mut self.proto.field, parent, Some(descriptor));
375 }
376
377 pub fn prepend_rule_path(&mut self, parent: &str) {
384 if parent.is_empty() {
385 return;
386 }
387 let current = self.rule_path();
388 if current.is_empty() {
389 self.set_rule_path(parent.to_string());
390 } else {
391 self.set_rule_path(format!("{parent}.{current}"));
392 }
393 }
394}
395
396fn prepend_with_subscript(
404 path: &mut Option<FieldPath>,
405 parent: &str,
406 subscript: field_path_element::Subscript,
407) {
408 let mut prefix_element = FieldPathElement {
409 field_name: Some(parent.to_string()),
410 subscript: Some(subscript),
411 ..FieldPathElement::default()
412 };
413
414 let suffix_elements = match path.take() {
415 Some(existing) => existing.elements,
416 None => Vec::new(),
417 };
418
419 let mut iter = suffix_elements.into_iter();
420 let mut merged = Vec::with_capacity(iter.size_hint().0 + 1);
421
422 if let Some(first) = iter.next() {
423 if is_subscript_only_element(&first) && prefix_element.subscript.is_none() {
424 prefix_element.subscript.clone_from(&first.subscript);
425 } else {
426 merged.push(first);
427 }
428 }
429 merged.insert(0, prefix_element);
430 merged.extend(iter);
431
432 *path = Some(FieldPath { elements: merged });
433}
434
435fn apply_field_descriptor_to_path(path: &mut Option<FieldPath>, desc: &FieldDescriptor) {
436 if let Some(path) = path.as_mut() {
437 if let Some(first) = path.elements.first_mut() {
438 let subscript = normalize_subscript_for_descriptor(first.subscript.take(), desc);
439 *first = field_path_element_from_descriptor(desc);
440 first.subscript = subscript;
441 apply_map_metadata(first, desc);
442 } else {
443 path.elements.push(field_path_element_from_descriptor(desc));
444 }
445 } else {
446 *path = Some(FieldPath {
447 elements: vec![field_path_element_from_descriptor(desc)],
448 });
449 }
450}
451
452fn hydrate_and_patch_rule_path(
453 path: &mut Option<FieldPath>,
454 extension_element: Option<&FieldPathElement>,
455) {
456 hydrate_rule_path(path);
457 if let (Some(ext), Some(path)) = (extension_element, path.as_mut()) {
460 if let Some(ext_name) = &ext.field_name {
461 for el in &mut path.elements {
462 if el.field_name.as_deref() == Some(ext_name) {
463 el.field_number = ext.field_number;
464 el.field_type = ext.field_type;
465 }
466 }
467 }
468 }
469}
470
471fn field_path_element_from_descriptor(desc: &FieldDescriptor) -> FieldPathElement {
472 FieldPathElement {
473 field_number: i32::try_from(desc.number()).ok(),
474 field_name: Some(desc.name().to_string()),
475 field_type: Some(if desc.is_group() {
476 prost_types::field_descriptor_proto::Type::Group
477 } else {
478 kind_to_descriptor_type(&desc.kind())
479 } as i32),
480 key_type: None,
481 value_type: None,
482 subscript: None,
483 }
484}
485
486fn apply_map_metadata(element: &mut FieldPathElement, desc: &FieldDescriptor) {
489 if desc.is_map() && element.subscript.is_some() {
490 let (key_type, value_type) = map_key_value_types(desc);
491 element.key_type = key_type;
492 element.value_type = value_type;
493 }
494}
495
496fn map_key_value_types(desc: &FieldDescriptor) -> (Option<i32>, Option<i32>) {
498 let kind = desc.kind();
499 let Some(entry) = kind.as_message() else {
500 return (None, None);
501 };
502 let key_type = entry
503 .get_field_by_name("key")
504 .map(|f| kind_to_descriptor_type(&f.kind()) as i32);
505 let value_type = entry
506 .get_field_by_name("value")
507 .map(|f| kind_to_descriptor_type(&f.kind()) as i32);
508 (key_type, value_type)
509}
510
511fn normalize_subscript_for_descriptor(
512 subscript: Option<field_path_element::Subscript>,
513 desc: &FieldDescriptor,
514) -> Option<field_path_element::Subscript> {
515 let subscript = subscript?;
516
517 if !desc.is_map() {
518 return Some(subscript);
519 }
520
521 let kind = desc.kind();
522 let Some(entry_desc) = kind.as_message() else {
523 return Some(subscript);
524 };
525 let Some(key_field) = entry_desc.get_field_by_name("key") else {
526 return Some(subscript);
527 };
528
529 match (subscript, key_field.kind()) {
530 (
531 field_path_element::Subscript::Index(value),
532 Kind::Int32
533 | Kind::Int64
534 | Kind::Sint32
535 | Kind::Sint64
536 | Kind::Sfixed32
537 | Kind::Sfixed64,
538 ) => i64::try_from(value)
539 .map(field_path_element::Subscript::IntKey)
540 .ok()
541 .or(Some(field_path_element::Subscript::Index(value))),
542 (
543 field_path_element::Subscript::Index(value),
544 Kind::Uint32 | Kind::Uint64 | Kind::Fixed32 | Kind::Fixed64,
545 ) => Some(field_path_element::Subscript::UintKey(value)),
546 (subscript, _) => Some(subscript),
547 }
548}
549
550pub(crate) fn kind_to_descriptor_type(kind: &Kind) -> prost_types::field_descriptor_proto::Type {
551 match *kind {
552 Kind::Double => prost_types::field_descriptor_proto::Type::Double,
553 Kind::Float => prost_types::field_descriptor_proto::Type::Float,
554 Kind::Int64 => prost_types::field_descriptor_proto::Type::Int64,
555 Kind::Uint64 => prost_types::field_descriptor_proto::Type::Uint64,
556 Kind::Int32 => prost_types::field_descriptor_proto::Type::Int32,
557 Kind::Fixed64 => prost_types::field_descriptor_proto::Type::Fixed64,
558 Kind::Fixed32 => prost_types::field_descriptor_proto::Type::Fixed32,
559 Kind::Bool => prost_types::field_descriptor_proto::Type::Bool,
560 Kind::String => prost_types::field_descriptor_proto::Type::String,
561 Kind::Message(_) => prost_types::field_descriptor_proto::Type::Message,
562 Kind::Bytes => prost_types::field_descriptor_proto::Type::Bytes,
563 Kind::Uint32 => prost_types::field_descriptor_proto::Type::Uint32,
564 Kind::Enum(_) => prost_types::field_descriptor_proto::Type::Enum,
565 Kind::Sfixed32 => prost_types::field_descriptor_proto::Type::Sfixed32,
566 Kind::Sfixed64 => prost_types::field_descriptor_proto::Type::Sfixed64,
567 Kind::Sint32 => prost_types::field_descriptor_proto::Type::Sint32,
568 Kind::Sint64 => prost_types::field_descriptor_proto::Type::Sint64,
569 }
570}
571
572fn prepend_proto_field_path(
573 path: &mut Option<FieldPath>,
574 parent: &str,
575 descriptor: Option<&FieldDescriptor>,
576) {
577 let Some(mut prefix) = parse_path(parent) else {
578 return;
579 };
580
581 if let Some(descriptor) = descriptor {
582 if let Some(first) = prefix.elements.first_mut() {
583 let subscript = normalize_subscript_for_descriptor(first.subscript.take(), descriptor);
584 *first = field_path_element_from_descriptor(descriptor);
585 first.subscript = subscript;
586 apply_map_metadata(first, descriptor);
587 } else {
588 prefix
589 .elements
590 .push(field_path_element_from_descriptor(descriptor));
591 }
592 }
593
594 let Some(mut suffix) = path.take() else {
595 *path = Some(prefix);
596 return;
597 };
598
599 if let (Some(last_prefix), Some(first_suffix)) =
600 (prefix.elements.last_mut(), suffix.elements.first())
601 {
602 if is_subscript_only_element(first_suffix) && last_prefix.subscript.is_none() {
603 last_prefix.subscript.clone_from(&first_suffix.subscript);
604 suffix.elements.remove(0);
605 if let Some(descriptor) = descriptor {
607 last_prefix.subscript =
608 normalize_subscript_for_descriptor(last_prefix.subscript.take(), descriptor);
609 apply_map_metadata(last_prefix, descriptor);
610 }
611 }
612 }
613
614 prefix.elements.extend(suffix.elements);
615 *path = Some(prefix);
616}
617
618fn is_subscript_only_element(element: &FieldPathElement) -> bool {
619 element.field_name.is_none()
620 && element.field_number.is_none()
621 && element.field_type.is_none()
622 && element.key_type.is_none()
623 && element.value_type.is_none()
624 && element.subscript.is_some()
625}
626
627fn parse_path(path: &str) -> Option<FieldPath> {
628 if path.is_empty() {
629 return None;
630 }
631
632 let mut elements = Vec::new();
633 for segment in split_segments(path) {
634 let (name, subscripts) = split_name_and_subscripts(segment);
635
636 if name.is_empty()
641 && subscripts.is_empty()
642 && segment.starts_with('[')
643 && segment.ends_with(']')
644 {
645 elements.push(FieldPathElement {
646 field_name: Some(segment.to_string()),
647 ..FieldPathElement::default()
648 });
649 continue;
650 }
651
652 if !name.is_empty() || subscripts.is_empty() {
653 elements.push(FieldPathElement {
654 field_name: if name.is_empty() { None } else { Some(name) },
655 ..FieldPathElement::default()
656 });
657 }
658
659 for (idx, subscript) in subscripts.into_iter().enumerate() {
660 if idx == 0 && !elements.is_empty() {
661 if let Some(last) = elements.last_mut() {
662 last.subscript = Some(subscript);
663 }
664 } else {
665 elements.push(FieldPathElement {
666 subscript: Some(subscript),
667 ..FieldPathElement::default()
668 });
669 }
670 }
671 }
672
673 Some(FieldPath { elements })
674}
675
676fn split_segments(path: &str) -> Vec<&str> {
677 let mut segments = Vec::new();
678 let mut start = 0usize;
679 let mut depth = 0usize;
680
681 for (idx, ch) in path.char_indices() {
682 match ch {
683 '[' => depth += 1,
684 ']' => depth = depth.saturating_sub(1),
685 '.' if depth == 0 => {
686 segments.push(&path[start..idx]);
687 start = idx + 1;
688 }
689 _ => {}
690 }
691 }
692
693 if start < path.len() {
694 segments.push(&path[start..]);
695 }
696
697 segments
698}
699
700fn split_name_and_subscripts(segment: &str) -> (String, Vec<field_path_element::Subscript>) {
701 let name_end = segment.find('[').unwrap_or(segment.len());
702 let name = segment[..name_end].to_string();
703 let mut subscripts = Vec::new();
704 let mut rest = &segment[name_end..];
705
706 while let Some(open_idx) = rest.find('[') {
707 let Some(close_rel) = rest[open_idx + 1..].find(']') else {
708 break;
709 };
710 let close_idx = open_idx + 1 + close_rel;
711 let token = &rest[open_idx + 1..close_idx];
712 if let Some(subscript) = parse_subscript(token) {
713 subscripts.push(subscript);
714 }
715 rest = &rest[close_idx + 1..];
716 }
717
718 (name, subscripts)
719}
720
721fn parse_subscript(token: &str) -> Option<field_path_element::Subscript> {
722 if token.starts_with('"') && token.ends_with('"') && token.len() >= 2 {
723 if let Ok(decoded) = serde_json::from_str::<String>(token) {
724 return Some(field_path_element::Subscript::StringKey(decoded));
725 }
726 }
727
728 if token.eq_ignore_ascii_case("true") {
729 return Some(field_path_element::Subscript::BoolKey(true));
730 }
731
732 if token.eq_ignore_ascii_case("false") {
733 return Some(field_path_element::Subscript::BoolKey(false));
734 }
735
736 if let Ok(index) = token.parse::<u64>() {
737 return Some(field_path_element::Subscript::Index(index));
738 }
739
740 if let Ok(int_key) = token.parse::<i64>() {
741 return Some(field_path_element::Subscript::IntKey(int_key));
742 }
743
744 None
745}
746
747fn hydrate_rule_path(path: &mut Option<FieldPath>) {
750 let Some(path) = path.as_mut() else {
751 return;
752 };
753 let Some(mut descriptor) = FIELD_RULES_DESCRIPTOR.clone() else {
754 return;
755 };
756 for element in &mut path.elements {
757 let Some(name) = element.field_name.as_deref() else {
758 continue;
759 };
760 if name.starts_with('[') {
765 continue;
766 }
767 let Some(field) = descriptor.get_field_by_name(name) else {
768 break;
769 };
770 element.field_number = i32::try_from(field.number()).ok();
771 element.field_type = if field.is_group() {
772 Some(prost_types::field_descriptor_proto::Type::Group as i32)
773 } else {
774 Some(kind_to_descriptor_type(&field.kind()) as i32)
775 };
776 if let Some(msg) = field.kind().as_message() {
777 descriptor = msg.clone();
778 }
779 }
780}
781
782fn field_path_string(path: Option<&FieldPath>) -> String {
783 let Some(path) = path else {
784 return String::new();
785 };
786
787 let mut out = String::new();
788 for element in &path.elements {
789 if let Some(name) = &element.field_name {
790 if !name.is_empty() {
791 if !out.is_empty() {
797 out.push('.');
798 }
799 out.push_str(name);
800 }
801 }
802
803 if let Some(subscript) = &element.subscript {
804 out.push('[');
805 match subscript {
806 field_path_element::Subscript::Index(i)
807 | field_path_element::Subscript::UintKey(i) => out.push_str(&i.to_string()),
808 field_path_element::Subscript::BoolKey(b) => out.push_str(&b.to_string()),
809 field_path_element::Subscript::IntKey(i) => out.push_str(&i.to_string()),
810 field_path_element::Subscript::StringKey(s) => {
811 let encoded = serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string());
812 out.push_str(&encoded);
813 }
814 }
815 out.push(']');
816 }
817 }
818
819 out
820}
821
822impl fmt::Display for Violation {
823 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
824 let has_path = self
825 .proto
826 .field
827 .as_ref()
828 .is_some_and(|p| !p.elements.is_empty());
829
830 if has_path {
831 write!(f, "{}: ", self.field_path())?;
832 }
833 if !self.message().is_empty() {
834 write!(f, "{}", self.message())
835 } else if !self.rule_id().is_empty() {
836 write!(f, "[{}]", self.rule_id())
837 } else {
838 write!(f, "[unknown]")
839 }
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use std::fmt::Write;
846
847 use pretty_assertions::assert_eq;
848 use proptest::collection::vec;
849 use proptest::prelude::*;
850
851 use super::{Violation, field_path_string, parse_path};
852
853 fn descriptor_field(message: &str, field: &str) -> prost_reflect::FieldDescriptor {
854 prost_protovalidate_types::DESCRIPTOR_POOL
855 .get_message_by_name(message)
856 .and_then(|message| message.get_field_by_name(field))
857 .expect("descriptor field must exist")
858 }
859
860 #[test]
861 fn prepend_path_with_descriptor_preserves_nested_descriptor_metadata() {
862 let parent = descriptor_field("buf.validate.FieldRules", "string");
863 let child = descriptor_field("buf.validate.StringRules", "min_len");
864
865 let mut violation = Violation::new("min_len", "string.min_len", "must be >= 1")
866 .with_field_descriptor(&child);
867 violation.prepend_path_with_descriptor("string", &parent);
868
869 let path = violation
870 .proto
871 .field
872 .as_ref()
873 .expect("field path should be populated");
874 assert_eq!(path.elements.len(), 2);
875
876 let parent_element = &path.elements[0];
877 assert_eq!(parent_element.field_name.as_deref(), Some("string"));
878 assert_eq!(
879 parent_element.field_number,
880 i32::try_from(parent.number()).ok()
881 );
882
883 let child_element = &path.elements[1];
884 assert_eq!(child_element.field_name.as_deref(), Some("min_len"));
885 assert_eq!(
886 child_element.field_number,
887 i32::try_from(child.number()).ok()
888 );
889 }
890
891 #[test]
892 fn field_path_string_round_trips_json_escaped_subscripts() {
893 let raw = "line\n\t\"quote\"\\slash";
894 let encoded = serde_json::to_string(raw).expect("json encoding should succeed");
895 let mut violation = Violation::new(format!("[{encoded}]"), "string.min_len", "bad");
896 violation.prepend_field_path("rules");
897
898 let rendered = field_path_string(violation.proto.field.as_ref());
899 assert_eq!(rendered, format!("rules[{encoded}]"));
900 }
901
902 #[test]
903 fn field_path_string_uses_proper_json_escaping_for_map_keys() {
904 let raw = "line\nvalue";
905 let encoded = serde_json::to_string(raw).expect("json encoding should succeed");
906 let violation = Violation::new(
907 format!("pattern[{encoded}]"),
908 "string.pattern",
909 "must match pattern",
910 );
911 assert_eq!(
912 field_path_string(violation.proto.field.as_ref()),
913 format!("pattern[{encoded}]")
914 );
915 }
916
917 #[test]
918 fn field_path_string_inserts_dot_after_map_subscript() {
919 let mut violation = Violation::new("value", "string.min_len", "must be >= 1");
924 violation.prepend_string_key("items", "alpha");
925
926 assert_eq!(
927 field_path_string(violation.proto.field.as_ref()),
928 "items[\"alpha\"].value",
929 );
930 }
931
932 #[test]
933 fn field_path_string_inserts_dot_after_repeated_subscript() {
934 let mut violation = Violation::new("name", "string.min_len", "must be >= 1");
936 violation.prepend_index("xs", 0);
937
938 assert_eq!(
939 field_path_string(violation.proto.field.as_ref()),
940 "xs[0].name",
941 );
942 }
943
944 #[test]
945 fn violation_display_prefers_field_and_message_then_rule_id_then_unknown() {
946 let with_path_and_message = Violation::new("one.two", "bar", "foo");
947 assert_eq!(with_path_and_message.to_string(), "one.two: foo");
948
949 let message_only = Violation::new("", "bar", "foo");
950 assert_eq!(message_only.to_string(), "foo");
951
952 let rule_id_only = Violation::new("", "bar", "");
953 assert_eq!(rule_id_only.to_string(), "[bar]");
954
955 let unknown = Violation::new("", "", "");
956 assert_eq!(unknown.to_string(), "[unknown]");
957 }
958
959 #[test]
960 fn hydrate_rule_path_populates_field_number_and_type() {
961 let violation = Violation::new("val", "int32.const", "must equal 1");
962 let rule = violation
963 .proto
964 .rule
965 .as_ref()
966 .expect("rule path should be populated");
967
968 assert_eq!(rule.elements.len(), 2);
969
970 let first = &rule.elements[0];
971 assert_eq!(first.field_name.as_deref(), Some("int32"));
972 assert!(
973 first.field_number.is_some(),
974 "int32 element must have field_number"
975 );
976 assert!(
977 first.field_type.is_some(),
978 "int32 element must have field_type"
979 );
980
981 let second = &rule.elements[1];
982 assert_eq!(second.field_name.as_deref(), Some("const"));
983 assert!(
984 second.field_number.is_some(),
985 "const element must have field_number"
986 );
987 assert!(
988 second.field_type.is_some(),
989 "const element must have field_type"
990 );
991 }
992
993 #[test]
994 fn hydrate_rule_path_handles_unknown_names_gracefully() {
995 let violation = Violation::new("val", "nonexistent.field", "message");
996 let rule = violation
997 .proto
998 .rule
999 .as_ref()
1000 .expect("rule path should be populated");
1001
1002 let first = &rule.elements[0];
1004 assert_eq!(first.field_name.as_deref(), Some("nonexistent"));
1005 assert_eq!(first.field_number, None);
1006 }
1007
1008 proptest! {
1009 #[test]
1010 fn dotted_paths_round_trip_through_parser(
1011 segments in vec("[a-zA-Z_][a-zA-Z0-9_]{0,8}", 1..6)
1012 ) {
1013 let path = segments.join(".");
1014 let parsed = parse_path(&path);
1015 prop_assert_eq!(field_path_string(parsed.as_ref()), path);
1016 }
1017
1018 #[test]
1019 fn indexed_paths_round_trip_through_parser(
1020 name in "[a-zA-Z_][a-zA-Z0-9_]{0,8}",
1021 indexes in vec(0_u16..1000, 1..4)
1022 ) {
1023 let mut path = name;
1024 for index in &indexes {
1025 let _ = write!(path, "[{index}]");
1026 }
1027 let parsed = parse_path(&path);
1028 prop_assert_eq!(field_path_string(parsed.as_ref()), path);
1029 }
1030 }
1031}