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 pub(crate) fn violations_mut(&mut self) -> &mut Vec<Violation> {
87 &mut self.violations
88 }
89
90 #[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#[derive(Debug, thiserror::Error)]
101#[error("compilation error: {cause}")]
102pub struct CompilationError {
103 pub cause: String,
105}
106
107#[derive(Debug, thiserror::Error)]
109#[error("runtime error: {cause}")]
110pub struct RuntimeError {
111 pub cause: String,
113}
114
115pub(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}