Skip to main content

prost_protovalidate/
violation.rs

1use std::fmt;
2
3use prost_protovalidate_types::{FieldPath, FieldPathElement, field_path_element};
4use prost_reflect::{FieldDescriptor, Kind, Value};
5
6/// A single instance where a validation rule was not met.
7#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub struct Violation {
10    /// The dot-separated field path where the violation occurred (e.g. `email`, `home.lat`).
11    pub field_path: String,
12
13    /// The dot-separated rule path that was violated (e.g. `string.min_len`).
14    pub rule_path: String,
15
16    /// Machine-readable constraint identifier (e.g. `string.min_len`, `required`).
17    pub rule_id: String,
18
19    /// Human-readable violation message.
20    pub message: String,
21
22    /// The field descriptor for the violated field, if available.
23    pub field_descriptor: Option<FieldDescriptor>,
24
25    /// The field value that failed validation, when available.
26    pub field_value: Option<Value>,
27
28    /// The descriptor for the violated rule field, when available.
29    pub rule_descriptor: Option<FieldDescriptor>,
30
31    /// The value of the violated rule field, when available.
32    pub rule_value: Option<Value>,
33
34    /// Wire-compatible violation payload.
35    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    /// Prepend a parent field path element.
121    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    /// Prepend a parent rule path element.
144    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}