Skip to main content

prost_protovalidate/
error.rs

1use std::fmt;
2
3use crate::violation::Violation;
4
5/// Top-level error type returned by validation.
6#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum Error {
9    /// One or more validation rules were violated.
10    #[error(transparent)]
11    Validation(#[from] ValidationError),
12
13    /// A validation rule could not be compiled.
14    #[error(transparent)]
15    Compilation(#[from] CompilationError),
16
17    /// A runtime failure occurred while executing a dynamic rule (e.g. CEL).
18    #[error(transparent)]
19    Runtime(#[from] RuntimeError),
20}
21
22/// Returned when one or more validation rules are violated.
23#[derive(Debug)]
24pub struct ValidationError {
25    /// The list of constraint violations found during validation.
26    violations: Vec<Violation>,
27}
28
29impl fmt::Display for ValidationError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self.violations.len() {
32            0 => Ok(()),
33            1 => write!(f, "validation error: {}", self.violations[0]),
34            _ => {
35                write!(f, "validation errors:")?;
36                for v in &self.violations {
37                    write!(f, "\n - {v}")?;
38                }
39                Ok(())
40            }
41        }
42    }
43}
44
45impl std::error::Error for ValidationError {}
46
47impl ValidationError {
48    /// Create a validation error from a list of violations.
49    #[must_use]
50    pub fn new(violations: Vec<Violation>) -> Self {
51        Self { violations }
52    }
53
54    /// Create a validation error containing a single violation.
55    #[must_use]
56    pub fn single(violation: Violation) -> Self {
57        Self {
58            violations: vec![violation],
59        }
60    }
61
62    /// Returns all violations as a shared slice.
63    #[must_use]
64    pub fn violations(&self) -> &[Violation] {
65        &self.violations
66    }
67
68    /// Consume and return the underlying violations vector.
69    #[must_use]
70    pub fn into_violations(self) -> Vec<Violation> {
71        self.violations
72    }
73
74    /// Returns true when there are no violations.
75    #[must_use]
76    pub fn is_empty(&self) -> bool {
77        self.violations.is_empty()
78    }
79
80    /// Returns the number of violations.
81    #[must_use]
82    pub fn len(&self) -> usize {
83        self.violations.len()
84    }
85
86    pub(crate) fn violations_mut(&mut self) -> &mut Vec<Violation> {
87        &mut self.violations
88    }
89
90    /// Convert to the wire-compatible `buf.validate.Violations` message.
91    #[must_use]
92    pub fn to_proto(&self) -> prost_protovalidate_types::Violations {
93        prost_protovalidate_types::Violations {
94            violations: self.violations.iter().map(Violation::to_proto).collect(),
95        }
96    }
97}
98
99/// Returned when a validation rule cannot be compiled from its descriptor.
100#[derive(Debug, thiserror::Error)]
101#[error("compilation error: {cause}")]
102pub struct CompilationError {
103    /// Description of why the rule failed to compile.
104    pub cause: String,
105}
106
107/// Returned when runtime evaluation of dynamic rules fails.
108#[derive(Debug, thiserror::Error)]
109#[error("runtime error: {cause}")]
110pub struct RuntimeError {
111    /// Description of the runtime failure.
112    pub cause: String,
113}
114
115/// Merge violations from a sub-evaluation into an accumulator.
116///
117/// Returns `(should_continue, accumulated_error)`.
118/// If `fail_fast` is true, stops on the first violation.
119pub(crate) fn merge_violations(
120    acc: Option<Error>,
121    new_err: Result<(), Error>,
122    fail_fast: bool,
123) -> (bool, Option<Error>) {
124    let new_err = match new_err {
125        Ok(()) => return (true, acc),
126        Err(e) => e,
127    };
128
129    match new_err {
130        Error::Compilation(_) | Error::Runtime(_) => (false, Some(new_err)),
131        Error::Validation(new_val) => {
132            if fail_fast {
133                return (false, Some(Error::Validation(new_val)));
134            }
135            match acc {
136                Some(Error::Validation(mut existing)) => {
137                    existing.violations_mut().extend(new_val.into_violations());
138                    (true, Some(Error::Validation(existing)))
139                }
140                _ => (true, Some(Error::Validation(new_val))),
141            }
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use pretty_assertions::assert_eq;
149
150    use super::{Error, ValidationError, merge_violations};
151    use crate::violation::Violation;
152
153    fn validation_error(rule_id: &str) -> Error {
154        Error::Validation(ValidationError::single(Violation::new("", rule_id, "")))
155    }
156
157    #[test]
158    fn validation_error_display_matches_single_and_multiple_formats() {
159        let single = ValidationError::new(vec![Violation::new("one.two", "bar", "foo")]);
160        assert_eq!(single.to_string(), "validation error: one.two: foo");
161
162        let multiple = ValidationError::new(vec![
163            Violation::new("one.two", "bar", "foo"),
164            Violation::new("one.three", "bar", ""),
165        ]);
166        assert_eq!(
167            multiple.to_string(),
168            "validation errors:\n - one.two: foo\n - one.three: [bar]"
169        );
170    }
171
172    #[test]
173    fn merge_violations_handles_non_validation_and_validation_paths() {
174        let (cont, acc) = merge_violations(None, Ok(()), true);
175        assert!(cont);
176        assert!(acc.is_none());
177
178        let runtime = Error::Runtime(super::RuntimeError {
179            cause: "runtime failure".to_string(),
180        });
181        let (cont, acc) = merge_violations(None, Err(runtime), false);
182        assert!(!cont);
183        assert!(matches!(acc, Some(Error::Runtime(_))));
184
185        let (cont, acc) = merge_violations(None, Err(validation_error("foo")), true);
186        assert!(!cont);
187        let Some(Error::Validation(err)) = acc else {
188            panic!("expected validation error");
189        };
190        assert_eq!(err.len(), 1);
191        assert_eq!(err.violations()[0].rule_id(), "foo");
192
193        let base = Some(validation_error("foo"));
194        let (cont, acc) = merge_violations(base, Err(validation_error("bar")), false);
195        assert!(cont);
196        let Some(Error::Validation(err)) = acc else {
197            panic!("expected merged validation error");
198        };
199        assert_eq!(err.len(), 2);
200        assert_eq!(err.violations()[0].rule_id(), "foo");
201        assert_eq!(err.violations()[1].rule_id(), "bar");
202    }
203
204    #[test]
205    fn validation_error_to_proto_reflects_post_construction_mutation() {
206        let mut violation = Violation::new("one.two", "string.min_len", "must be >= 2");
207        violation.set_field_path("updated.path");
208        violation.set_rule_path("string.max_len");
209        violation.set_rule_id("string.max_len");
210        violation.set_message("must be <= 10");
211
212        let proto = ValidationError::new(vec![violation]).to_proto();
213        assert_eq!(proto.violations.len(), 1);
214
215        let first = &proto.violations[0];
216        let field_name = first
217            .field
218            .as_ref()
219            .and_then(|path| path.elements.first())
220            .and_then(|element| element.field_name.as_deref());
221        assert_eq!(field_name, Some("updated"));
222        assert_eq!(first.rule_id.as_deref(), Some("string.max_len"));
223        assert_eq!(first.message.as_deref(), Some("must be <= 10"));
224    }
225}