1use std::fmt;
2
3use prost_protovalidate_types::{FieldPath, FieldPathElement, field_path_element};
4use prost_reflect::{FieldDescriptor, Kind, Value};
5
6#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub struct Violation {
10 pub field_path: String,
12
13 pub rule_path: String,
15
16 pub rule_id: String,
18
19 pub message: String,
21
22 pub field_descriptor: Option<FieldDescriptor>,
24
25 pub field_value: Option<Value>,
27
28 pub rule_descriptor: Option<FieldDescriptor>,
30
31 pub rule_value: Option<Value>,
33
34 pub proto: prost_protovalidate_types::Violation,
36}
37
38impl Violation {
39 pub(crate) fn new(
40 field_path: impl Into<String>,
41 rule_id: impl Into<String>,
42 message: impl Into<String>,
43 ) -> Self {
44 let rule_id = rule_id.into();
45 let mut out = Self {
46 field_path: field_path.into(),
47 rule_path: rule_id.clone(),
48 rule_id,
49 message: message.into(),
50 field_descriptor: None,
51 field_value: None,
52 rule_descriptor: None,
53 rule_value: None,
54 proto: prost_protovalidate_types::Violation::default(),
55 };
56 out.sync_proto();
57 out
58 }
59
60 fn sync_proto(&mut self) {
61 if self.proto.field.is_none() {
62 self.proto.field = parse_path(&self.field_path);
63 }
64 self.proto.rule = parse_path(&self.rule_path);
65 self.proto.rule_id = if self.rule_id.is_empty() {
66 None
67 } else {
68 Some(self.rule_id.clone())
69 };
70 self.proto.message = if self.message.is_empty() {
71 None
72 } else {
73 Some(self.message.clone())
74 };
75 }
76
77 pub(crate) fn with_field_descriptor(mut self, desc: &FieldDescriptor) -> Self {
78 self.field_descriptor = Some(desc.clone());
79 if let Some(path) = self.proto.field.as_mut() {
80 if let Some(first) = path.elements.first_mut() {
81 let subscript = normalize_subscript_for_descriptor(first.subscript.take(), desc);
82 *first = field_path_element_from_descriptor(desc);
83 first.subscript = subscript;
84 } else {
85 path.elements.push(field_path_element_from_descriptor(desc));
86 }
87 } else {
88 self.proto.field = Some(FieldPath {
89 elements: vec![field_path_element_from_descriptor(desc)],
90 });
91 }
92 self
93 }
94
95 pub(crate) fn with_field_value(mut self, value: Value) -> Self {
96 self.field_value = Some(value);
97 self
98 }
99
100 pub(crate) fn with_rule_path(mut self, rule_path: impl Into<String>) -> Self {
101 self.rule_path = rule_path.into();
102 self.sync_proto();
103 self
104 }
105
106 pub(crate) fn with_rule_descriptor(mut self, descriptor: FieldDescriptor) -> Self {
107 self.rule_descriptor = Some(descriptor);
108 self
109 }
110
111 pub(crate) fn with_rule_value(mut self, value: Value) -> Self {
112 self.rule_value = Some(value);
113 self
114 }
115
116 pub(crate) fn mark_for_key(&mut self) {
117 self.proto.for_key = Some(true);
118 }
119
120 pub(crate) fn prepend_path(&mut self, parent: &str) {
122 if parent.is_empty() {
123 return;
124 }
125 self.field_path = prepend_path_string(parent, &self.field_path);
126 prepend_proto_field_path(&mut self.proto.field, parent, None);
127 self.sync_proto();
128 }
129
130 pub(crate) fn prepend_path_with_descriptor(
131 &mut self,
132 parent: &str,
133 descriptor: &FieldDescriptor,
134 ) {
135 if parent.is_empty() {
136 return;
137 }
138 self.field_path = prepend_path_string(parent, &self.field_path);
139 prepend_proto_field_path(&mut self.proto.field, parent, Some(descriptor));
140 self.sync_proto();
141 }
142
143 pub(crate) fn prepend_rule_path(&mut self, parent: &str) {
145 if parent.is_empty() {
146 return;
147 }
148 if self.rule_path.is_empty() {
149 self.rule_path = parent.to_string();
150 } else {
151 self.rule_path = format!("{parent}.{}", self.rule_path);
152 }
153 self.sync_proto();
154 }
155}
156
157fn field_path_element_from_descriptor(desc: &FieldDescriptor) -> FieldPathElement {
158 let mut out = FieldPathElement {
159 field_number: i32::try_from(desc.number()).ok(),
160 field_name: Some(desc.name().to_string()),
161 field_type: Some(if desc.is_group() {
162 prost_types::field_descriptor_proto::Type::Group
163 } else {
164 kind_to_descriptor_type(&desc.kind())
165 } as i32),
166 key_type: None,
167 value_type: None,
168 subscript: None,
169 };
170
171 if desc.is_map() {
172 if let Some(entry) = desc.kind().as_message() {
173 if let Some(key_field) = entry.get_field_by_name("key") {
174 out.key_type = Some(kind_to_descriptor_type(&key_field.kind()) as i32);
175 }
176 if let Some(value_field) = entry.get_field_by_name("value") {
177 out.value_type = Some(kind_to_descriptor_type(&value_field.kind()) as i32);
178 }
179 }
180 }
181
182 out
183}
184
185fn normalize_subscript_for_descriptor(
186 subscript: Option<field_path_element::Subscript>,
187 desc: &FieldDescriptor,
188) -> Option<field_path_element::Subscript> {
189 let subscript = subscript?;
190
191 if !desc.is_map() {
192 return Some(subscript);
193 }
194
195 let kind = desc.kind();
196 let Some(entry_desc) = kind.as_message() else {
197 return Some(subscript);
198 };
199 let Some(key_field) = entry_desc.get_field_by_name("key") else {
200 return Some(subscript);
201 };
202
203 match (subscript, key_field.kind()) {
204 (
205 field_path_element::Subscript::Index(value),
206 Kind::Int32
207 | Kind::Int64
208 | Kind::Sint32
209 | Kind::Sint64
210 | Kind::Sfixed32
211 | Kind::Sfixed64,
212 ) => i64::try_from(value)
213 .map(field_path_element::Subscript::IntKey)
214 .ok()
215 .or(Some(field_path_element::Subscript::Index(value))),
216 (
217 field_path_element::Subscript::Index(value),
218 Kind::Uint32 | Kind::Uint64 | Kind::Fixed32 | Kind::Fixed64,
219 ) => Some(field_path_element::Subscript::UintKey(value)),
220 (subscript, _) => Some(subscript),
221 }
222}
223
224fn kind_to_descriptor_type(kind: &Kind) -> prost_types::field_descriptor_proto::Type {
225 match *kind {
226 Kind::Double => prost_types::field_descriptor_proto::Type::Double,
227 Kind::Float => prost_types::field_descriptor_proto::Type::Float,
228 Kind::Int64 => prost_types::field_descriptor_proto::Type::Int64,
229 Kind::Uint64 => prost_types::field_descriptor_proto::Type::Uint64,
230 Kind::Int32 => prost_types::field_descriptor_proto::Type::Int32,
231 Kind::Fixed64 => prost_types::field_descriptor_proto::Type::Fixed64,
232 Kind::Fixed32 => prost_types::field_descriptor_proto::Type::Fixed32,
233 Kind::Bool => prost_types::field_descriptor_proto::Type::Bool,
234 Kind::String => prost_types::field_descriptor_proto::Type::String,
235 Kind::Message(_) => prost_types::field_descriptor_proto::Type::Message,
236 Kind::Bytes => prost_types::field_descriptor_proto::Type::Bytes,
237 Kind::Uint32 => prost_types::field_descriptor_proto::Type::Uint32,
238 Kind::Enum(_) => prost_types::field_descriptor_proto::Type::Enum,
239 Kind::Sfixed32 => prost_types::field_descriptor_proto::Type::Sfixed32,
240 Kind::Sfixed64 => prost_types::field_descriptor_proto::Type::Sfixed64,
241 Kind::Sint32 => prost_types::field_descriptor_proto::Type::Sint32,
242 Kind::Sint64 => prost_types::field_descriptor_proto::Type::Sint64,
243 }
244}
245
246fn prepend_path_string(parent: &str, current: &str) -> String {
247 if current.is_empty() {
248 return parent.to_string();
249 }
250 if current.starts_with('[') {
251 return format!("{parent}{current}");
252 }
253 format!("{parent}.{current}")
254}
255
256fn prepend_proto_field_path(
257 path: &mut Option<FieldPath>,
258 parent: &str,
259 descriptor: Option<&FieldDescriptor>,
260) {
261 let Some(mut prefix) = parse_path(parent) else {
262 return;
263 };
264
265 if let Some(descriptor) = descriptor {
266 if let Some(first) = prefix.elements.first_mut() {
267 let subscript = normalize_subscript_for_descriptor(first.subscript.take(), descriptor);
268 *first = field_path_element_from_descriptor(descriptor);
269 first.subscript = subscript;
270 } else {
271 prefix
272 .elements
273 .push(field_path_element_from_descriptor(descriptor));
274 }
275 }
276
277 let Some(mut suffix) = path.take() else {
278 *path = Some(prefix);
279 return;
280 };
281
282 if let (Some(last_prefix), Some(first_suffix)) =
283 (prefix.elements.last_mut(), suffix.elements.first())
284 {
285 if is_subscript_only_element(first_suffix) && last_prefix.subscript.is_none() {
286 last_prefix.subscript.clone_from(&first_suffix.subscript);
287 suffix.elements.remove(0);
288 }
289 }
290
291 prefix.elements.extend(suffix.elements);
292 *path = Some(prefix);
293}
294
295fn is_subscript_only_element(element: &FieldPathElement) -> bool {
296 element.field_name.is_none()
297 && element.field_number.is_none()
298 && element.field_type.is_none()
299 && element.key_type.is_none()
300 && element.value_type.is_none()
301 && element.subscript.is_some()
302}
303
304fn parse_path(path: &str) -> Option<FieldPath> {
305 if path.is_empty() {
306 return None;
307 }
308
309 let mut elements = Vec::new();
310 for segment in split_segments(path) {
311 let (name, subscripts) = split_name_and_subscripts(segment);
312
313 if !name.is_empty() || subscripts.is_empty() {
314 elements.push(FieldPathElement {
315 field_name: if name.is_empty() { None } else { Some(name) },
316 ..FieldPathElement::default()
317 });
318 }
319
320 for (idx, subscript) in subscripts.into_iter().enumerate() {
321 if idx == 0 && !elements.is_empty() {
322 if let Some(last) = elements.last_mut() {
323 last.subscript = Some(subscript);
324 }
325 } else {
326 elements.push(FieldPathElement {
327 subscript: Some(subscript),
328 ..FieldPathElement::default()
329 });
330 }
331 }
332 }
333
334 Some(FieldPath { elements })
335}
336
337fn split_segments(path: &str) -> Vec<&str> {
338 let mut segments = Vec::new();
339 let mut start = 0usize;
340 let mut depth = 0usize;
341
342 for (idx, ch) in path.char_indices() {
343 match ch {
344 '[' => depth += 1,
345 ']' => depth = depth.saturating_sub(1),
346 '.' if depth == 0 => {
347 segments.push(&path[start..idx]);
348 start = idx + 1;
349 }
350 _ => {}
351 }
352 }
353
354 if start < path.len() {
355 segments.push(&path[start..]);
356 }
357
358 segments
359}
360
361fn split_name_and_subscripts(segment: &str) -> (String, Vec<field_path_element::Subscript>) {
362 let name_end = segment.find('[').unwrap_or(segment.len());
363 let name = segment[..name_end].to_string();
364 let mut subscripts = Vec::new();
365 let mut rest = &segment[name_end..];
366
367 while let Some(open_idx) = rest.find('[') {
368 let Some(close_rel) = rest[open_idx + 1..].find(']') else {
369 break;
370 };
371 let close_idx = open_idx + 1 + close_rel;
372 let token = &rest[open_idx + 1..close_idx];
373 if let Some(subscript) = parse_subscript(token) {
374 subscripts.push(subscript);
375 }
376 rest = &rest[close_idx + 1..];
377 }
378
379 (name, subscripts)
380}
381
382fn parse_subscript(token: &str) -> Option<field_path_element::Subscript> {
383 if token.starts_with('"') && token.ends_with('"') && token.len() >= 2 {
384 if let Ok(decoded) = serde_json::from_str::<String>(token) {
385 return Some(field_path_element::Subscript::StringKey(decoded));
386 }
387 }
388
389 if token.eq_ignore_ascii_case("true") {
390 return Some(field_path_element::Subscript::BoolKey(true));
391 }
392
393 if token.eq_ignore_ascii_case("false") {
394 return Some(field_path_element::Subscript::BoolKey(false));
395 }
396
397 if let Ok(index) = token.parse::<u64>() {
398 return Some(field_path_element::Subscript::Index(index));
399 }
400
401 if let Ok(int_key) = token.parse::<i64>() {
402 return Some(field_path_element::Subscript::IntKey(int_key));
403 }
404
405 None
406}
407
408fn field_path_string(path: Option<&FieldPath>) -> String {
409 let Some(path) = path else {
410 return String::new();
411 };
412
413 let mut out = String::new();
414 for element in &path.elements {
415 if let Some(name) = &element.field_name {
416 if !name.is_empty() {
417 if !out.is_empty() && !out.ends_with(']') {
418 out.push('.');
419 }
420 out.push_str(name);
421 }
422 }
423
424 if let Some(subscript) = &element.subscript {
425 out.push('[');
426 match subscript {
427 field_path_element::Subscript::Index(i)
428 | field_path_element::Subscript::UintKey(i) => out.push_str(&i.to_string()),
429 field_path_element::Subscript::BoolKey(b) => out.push_str(&b.to_string()),
430 field_path_element::Subscript::IntKey(i) => out.push_str(&i.to_string()),
431 field_path_element::Subscript::StringKey(s) => {
432 let encoded = serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string());
433 out.push_str(&encoded);
434 }
435 }
436 out.push(']');
437 }
438 }
439
440 out
441}
442
443impl fmt::Display for Violation {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 let rendered_path = if self.field_path.is_empty() {
446 field_path_string(self.proto.field.as_ref())
447 } else {
448 self.field_path.clone()
449 };
450
451 if !rendered_path.is_empty() {
452 write!(f, "{rendered_path}: ")?;
453 }
454 if !self.message.is_empty() {
455 write!(f, "{}", self.message)
456 } else if !self.rule_id.is_empty() {
457 write!(f, "[{}]", self.rule_id)
458 } else {
459 write!(f, "[unknown]")
460 }
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::{Violation, field_path_string};
467
468 fn descriptor_field(message: &str, field: &str) -> prost_reflect::FieldDescriptor {
469 prost_protovalidate_types::DESCRIPTOR_POOL
470 .get_message_by_name(message)
471 .and_then(|message| message.get_field_by_name(field))
472 .expect("descriptor field must exist")
473 }
474
475 #[test]
476 fn prepend_path_with_descriptor_preserves_nested_descriptor_metadata() {
477 let parent = descriptor_field("buf.validate.FieldRules", "string");
478 let child = descriptor_field("buf.validate.StringRules", "min_len");
479
480 let mut violation = Violation::new("min_len", "string.min_len", "must be >= 1")
481 .with_field_descriptor(&child);
482 violation.prepend_path_with_descriptor("string", &parent);
483
484 let path = violation
485 .proto
486 .field
487 .as_ref()
488 .expect("field path should be populated");
489 assert_eq!(path.elements.len(), 2);
490
491 let parent_element = &path.elements[0];
492 assert_eq!(parent_element.field_name.as_deref(), Some("string"));
493 assert_eq!(
494 parent_element.field_number,
495 i32::try_from(parent.number()).ok()
496 );
497
498 let child_element = &path.elements[1];
499 assert_eq!(child_element.field_name.as_deref(), Some("min_len"));
500 assert_eq!(
501 child_element.field_number,
502 i32::try_from(child.number()).ok()
503 );
504 }
505
506 #[test]
507 fn field_path_string_round_trips_json_escaped_subscripts() {
508 let raw = "line\n\t\"quote\"\\slash";
509 let encoded = serde_json::to_string(raw).expect("json encoding should succeed");
510 let mut violation = Violation::new(format!("[{encoded}]"), "string.min_len", "bad");
511 violation.prepend_path("rules");
512
513 let rendered = field_path_string(violation.proto.field.as_ref());
514 assert_eq!(rendered, format!("rules[{encoded}]"));
515 }
516
517 #[test]
518 fn field_path_string_uses_proper_json_escaping_for_map_keys() {
519 let raw = "line\nvalue";
520 let encoded = serde_json::to_string(raw).expect("json encoding should succeed");
521 let violation = Violation::new(
522 format!("pattern[{encoded}]"),
523 "string.pattern",
524 "must match pattern",
525 );
526 assert_eq!(
527 field_path_string(violation.proto.field.as_ref()),
528 format!("pattern[{encoded}]")
529 );
530 }
531
532 #[test]
533 fn violation_display_prefers_field_and_message_then_rule_id_then_unknown() {
534 let with_path_and_message = Violation::new("one.two", "bar", "foo");
535 assert_eq!(with_path_and_message.to_string(), "one.two: foo");
536
537 let message_only = Violation::new("", "bar", "foo");
538 assert_eq!(message_only.to_string(), "foo");
539
540 let rule_id_only = Violation::new("", "bar", "");
541 assert_eq!(rule_id_only.to_string(), "[bar]");
542
543 let unknown = Violation::new("", "", "");
544 assert_eq!(unknown.to_string(), "[unknown]");
545 }
546}