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(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 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 #[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 #[must_use]
91 pub fn field_path(&self) -> String {
92 field_path_string(self.proto.field.as_ref())
93 }
94
95 #[must_use]
97 pub fn rule_path(&self) -> String {
98 field_path_string(self.proto.rule.as_ref())
99 }
100
101 #[must_use]
103 pub fn rule_id(&self) -> &str {
104 self.proto.rule_id.as_deref().unwrap_or("")
105 }
106
107 #[must_use]
109 pub fn message(&self) -> &str {
110 self.proto.message.as_deref().unwrap_or("")
111 }
112
113 #[must_use]
115 pub fn field_descriptor(&self) -> Option<&FieldDescriptor> {
116 self.field_descriptor.as_ref()
117 }
118
119 #[must_use]
121 pub fn field_value(&self) -> Option<&Value> {
122 self.field_value.as_ref()
123 }
124
125 #[must_use]
127 pub fn rule_descriptor(&self) -> Option<&FieldDescriptor> {
128 self.rule_descriptor.as_ref()
129 }
130
131 #[must_use]
133 pub fn rule_value(&self) -> Option<&Value> {
134 self.rule_value.as_ref()
135 }
136
137 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 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 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 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 pub(crate) fn with_rule_extension_element(mut self, element: FieldPathElement) -> Self {
226 self.extension_element = Some(element.clone());
228 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 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 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 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 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
335fn 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
345fn 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 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 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
596fn 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 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 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}