prost_protovalidate/
error.rs1use std::fmt;
2
3use crate::violation::Violation;
4
5#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum Error {
9 #[error(transparent)]
11 Validation(#[from] ValidationError),
12
13 #[error(transparent)]
15 Compilation(#[from] CompilationError),
16
17 #[error(transparent)]
19 Runtime(#[from] RuntimeError),
20}
21
22#[derive(Debug)]
24pub struct ValidationError {
25 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 #[must_use]
50 pub fn new(violations: Vec<Violation>) -> Self {
51 Self { violations }
52 }
53
54 #[must_use]
56 pub fn single(violation: Violation) -> Self {
57 Self {
58 violations: vec![violation],
59 }
60 }
61
62 #[must_use]
64 pub fn violations(&self) -> &[Violation] {
65 &self.violations
66 }
67
68 #[must_use]
70 pub fn into_violations(self) -> Vec<Violation> {
71 self.violations
72 }
73
74 #[must_use]
76 pub fn is_empty(&self) -> bool {
77 self.violations.is_empty()
78 }
79
80 #[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 #[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#[derive(Debug, thiserror::Error)]
102#[error("compilation error: {cause}")]
103pub struct CompilationError {
104 pub cause: String,
106}
107
108#[derive(Debug, thiserror::Error)]
110#[error("runtime error: {cause}")]
111pub struct RuntimeError {
112 pub cause: String,
114}
115
116#[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}