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    #[cfg_attr(not(feature = "reflect"), allow(dead_code))]
87    pub(crate) fn violations_mut(&mut self) -> &mut Vec<Violation> {
88        &mut self.violations
89    }
90
91    /// Convert to the wire-compatible `buf.validate.Violations` message.
92    #[must_use]
93    pub fn to_proto(&self) -> prost_protovalidate_types::Violations {
94        prost_protovalidate_types::Violations {
95            violations: self.violations.iter().map(Violation::to_proto).collect(),
96        }
97    }
98}
99
100/// Returned when a validation rule cannot be compiled from its descriptor.
101#[derive(Debug, thiserror::Error)]
102#[error("compilation error: {cause}")]
103pub struct CompilationError {
104    /// Description of why the rule failed to compile.
105    pub cause: String,
106}
107
108/// Returned when runtime evaluation of dynamic rules fails.
109#[derive(Debug, thiserror::Error)]
110#[error("runtime error: {cause}")]
111pub struct RuntimeError {
112    /// Description of the runtime failure.
113    pub cause: String,
114}
115
116/// Merge violations from a sub-evaluation into an accumulator.
117///
118/// Returns `(should_continue, accumulated_error)`.
119/// If `fail_fast` is true, stops on the first violation.
120#[cfg_attr(not(feature = "reflect"), allow(dead_code))]
121pub(crate) fn merge_violations(
122    acc: Option<Error>,
123    new_err: Result<(), Error>,
124    fail_fast: bool,
125) -> (bool, Option<Error>) {
126    let new_err = match new_err {
127        Ok(()) => return (true, acc),
128        Err(e) => e,
129    };
130
131    match new_err {
132        Error::Compilation(_) | Error::Runtime(_) => (false, Some(new_err)),
133        Error::Validation(new_val) => {
134            if fail_fast {
135                return (false, Some(Error::Validation(new_val)));
136            }
137            match acc {
138                Some(Error::Validation(mut existing)) => {
139                    existing.violations_mut().extend(new_val.into_violations());
140                    (true, Some(Error::Validation(existing)))
141                }
142                _ => (true, Some(Error::Validation(new_val))),
143            }
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use pretty_assertions::assert_eq;
151
152    use super::{Error, ValidationError, merge_violations};
153    use crate::violation::Violation;
154
155    fn validation_error(rule_id: &str) -> Error {
156        Error::Validation(ValidationError::single(Violation::new("", rule_id, "")))
157    }
158
159    #[test]
160    fn validation_error_display_matches_single_and_multiple_formats() {
161        let single = ValidationError::new(vec![Violation::new("one.two", "bar", "foo")]);
162        assert_eq!(single.to_string(), "validation error: one.two: foo");
163
164        let multiple = ValidationError::new(vec![
165            Violation::new("one.two", "bar", "foo"),
166            Violation::new("one.three", "bar", ""),
167        ]);
168        assert_eq!(
169            multiple.to_string(),
170            "validation errors:\n - one.two: foo\n - one.three: [bar]"
171        );
172    }
173
174    #[test]
175    fn merge_violations_handles_non_validation_and_validation_paths() {
176        let (cont, acc) = merge_violations(None, Ok(()), true);
177        assert!(cont);
178        assert!(acc.is_none());
179
180        let runtime = Error::Runtime(super::RuntimeError {
181            cause: "runtime failure".to_string(),
182        });
183        let (cont, acc) = merge_violations(None, Err(runtime), false);
184        assert!(!cont);
185        assert!(matches!(acc, Some(Error::Runtime(_))));
186
187        let (cont, acc) = merge_violations(None, Err(validation_error("foo")), true);
188        assert!(!cont);
189        let Some(Error::Validation(err)) = acc else {
190            panic!("expected validation error");
191        };
192        assert_eq!(err.len(), 1);
193        assert_eq!(err.violations()[0].rule_id(), "foo");
194
195        let base = Some(validation_error("foo"));
196        let (cont, acc) = merge_violations(base, Err(validation_error("bar")), false);
197        assert!(cont);
198        let Some(Error::Validation(err)) = acc else {
199            panic!("expected merged validation error");
200        };
201        assert_eq!(err.len(), 2);
202        assert_eq!(err.violations()[0].rule_id(), "foo");
203        assert_eq!(err.violations()[1].rule_id(), "bar");
204    }
205
206    #[test]
207    fn validation_error_to_proto_reflects_post_construction_mutation() {
208        let mut violation = Violation::new("one.two", "string.min_len", "must be >= 2");
209        violation.set_field_path("updated.path");
210        violation.set_rule_path("string.max_len");
211        violation.set_rule_id("string.max_len");
212        violation.set_message("must be <= 10");
213
214        let proto = ValidationError::new(vec![violation]).to_proto();
215        assert_eq!(proto.violations.len(), 1);
216
217        let first = &proto.violations[0];
218        let field_name = first
219            .field
220            .as_ref()
221            .and_then(|path| path.elements.first())
222            .and_then(|element| element.field_name.as_deref());
223        assert_eq!(field_name, Some("updated"));
224        assert_eq!(first.rule_id.as_deref(), Some("string.max_len"));
225        assert_eq!(first.message.as_deref(), Some("must be <= 10"));
226    }
227}