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#[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#[derive(Debug, Clone)]
33#[non_exhaustive]
34pub struct Violation {
35 proto: prost_protovalidate_types::Violation,
37
38 #[cfg(feature = "reflect")]
40 field_descriptor: Option<FieldDescriptor>,
41
42 #[cfg(feature = "reflect")]
44 field_value: Option<Value>,
45
46 #[cfg(feature = "reflect")]
48 rule_descriptor: Option<FieldDescriptor>,
49
50 #[cfg(feature = "reflect")]
52 rule_value: Option<Value>,
53
54 extension_element: Option<FieldPathElement>,
56}
57
58impl Violation {
59 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 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 #[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 #[must_use]
129 pub fn field_path(&self) -> String {
130 field_path_string(self.proto.field.as_ref())
131 }
132
133 #[must_use]
135 pub fn rule_path(&self) -> String {
136 field_path_string(self.proto.rule.as_ref())
137 }
138
139 #[must_use]
141 pub fn rule_id(&self) -> &str {
142 self.proto.rule_id.as_deref().unwrap_or("")
143 }
144
145 #[must_use]
147 pub fn message(&self) -> &str {
148 self.proto.message.as_deref().unwrap_or("")
149 }
150
151 #[cfg(feature = "reflect")]
153 #[must_use]
154 pub fn field_descriptor(&self) -> Option<&FieldDescriptor> {
155 self.field_descriptor.as_ref()
156 }
157
158 #[cfg(feature = "reflect")]
160 #[must_use]
161 pub fn field_value(&self) -> Option<&Value> {
162 self.field_value.as_ref()
163 }
164
165 #[cfg(feature = "reflect")]
167 #[must_use]
168 pub fn rule_descriptor(&self) -> Option<&FieldDescriptor> {
169 self.rule_descriptor.as_ref()
170 }
171
172 #[cfg(feature = "reflect")]
174 #[must_use]
175 pub fn rule_value(&self) -> Option<&Value> {
176 self.rule_value.as_ref()
177 }
178
179 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 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 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 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 #[cfg(feature = "cel")]
281 pub(crate) fn with_rule_extension_element(mut self, element: FieldPathElement) -> Self {
282 self.extension_element = Some(element.clone());
284 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 #[must_use]
301 pub fn without_rule_path(mut self) -> Self {
302 self.proto.rule = None;
303 self
304 }
305
306 pub fn mark_for_key(&mut self) {
312 self.proto.for_key = Some(true);
313 }
314
315 #[must_use]
320 pub fn for_key(&self) -> Option<bool> {
321 self.proto.for_key
322 }
323
324 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 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 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 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 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 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 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
429fn 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 #[cfg(feature = "reflect")]
493 hydrate_rule_path(path);
494 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#[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#[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
614fn 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#[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 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 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#[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 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 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 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 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 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}