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