1#[cfg(feature = "alloc")]
2use alloc::vec::Vec;
3use core::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub struct Violation {
9 pub field: Option<&'static str>,
11 pub message: &'static str,
13}
14
15impl Violation {
16 pub const fn new(message: &'static str) -> Self {
18 Self {
19 field: None,
20 message,
21 }
22 }
23
24 pub const fn with_field(field: &'static str, message: &'static str) -> Self {
26 Self {
27 field: Some(field),
28 message,
29 }
30 }
31}
32
33impl fmt::Display for Violation {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self.field {
36 Some(field) => write!(f, "{field}: {}", self.message),
37 None => f.write_str(self.message),
38 }
39 }
40}
41
42#[cfg(feature = "alloc")]
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ValidationError {
53 violations: Vec<Violation>,
54}
55
56#[cfg(feature = "alloc")]
58pub type ValidateResult<T = ()> = Result<T, ValidationError>;
59
60#[cfg(feature = "alloc")]
61impl ValidationError {
62 pub fn new(message: &'static str) -> Self {
64 Self {
65 violations: alloc::vec![Violation::new(message)],
66 }
67 }
68
69 pub fn field(field: &'static str, message: &'static str) -> Self {
71 Self {
72 violations: alloc::vec![Violation::with_field(field, message)],
73 }
74 }
75
76 pub fn empty() -> Self {
82 Self {
83 violations: Vec::new(),
84 }
85 }
86
87 pub fn with(mut self, violation: Violation) -> Self {
89 self.violations.push(violation);
90 self
91 }
92
93 pub fn push(&mut self, violation: Violation) {
95 self.violations.push(violation);
96 }
97
98 pub fn merge(mut self, other: Self) -> Self {
100 self.violations.extend(other.violations);
101 self
102 }
103
104 pub fn require(mut self, condition: bool, violation: Violation) -> Self {
121 if !condition {
122 self.violations.push(violation);
123 }
124 self
125 }
126
127 pub fn require_field(
130 self,
131 condition: bool,
132 field: &'static str,
133 message: &'static str,
134 ) -> Self {
135 self.require(condition, Violation::with_field(field, message))
136 }
137
138 pub fn finish(self) -> ValidateResult {
144 if self.is_empty() {
145 Ok(())
146 } else {
147 Err(self)
148 }
149 }
150
151 pub fn violations(&self) -> &[Violation] {
153 &self.violations
154 }
155
156 pub fn is_empty(&self) -> bool {
158 self.violations.is_empty()
159 }
160
161 pub fn len(&self) -> usize {
163 self.violations.len()
164 }
165}
166
167#[cfg(feature = "alloc")]
168impl From<Violation> for ValidationError {
169 fn from(v: Violation) -> Self {
170 Self {
171 violations: alloc::vec![v],
172 }
173 }
174}
175
176#[cfg(feature = "alloc")]
177impl From<&'static str> for ValidationError {
178 fn from(message: &'static str) -> Self {
179 Self::new(message)
180 }
181}
182
183#[cfg(feature = "alloc")]
184impl FromIterator<Violation> for ValidationError {
185 fn from_iter<I: IntoIterator<Item = Violation>>(iter: I) -> Self {
186 Self {
187 violations: iter.into_iter().collect(),
188 }
189 }
190}
191
192#[cfg(feature = "alloc")]
193impl Extend<Violation> for ValidationError {
194 fn extend<I: IntoIterator<Item = Violation>>(&mut self, iter: I) {
195 self.violations.extend(iter);
196 }
197}
198
199#[cfg(feature = "alloc")]
200impl fmt::Display for ValidationError {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 match self.violations.as_slice() {
203 [] => f.write_str("validation failed"),
204 [single] => fmt::Display::fmt(single, f),
205 violations => {
206 for (i, v) in violations.iter().enumerate() {
207 if i > 0 {
208 write!(f, "; ")?;
209 }
210 fmt::Display::fmt(v, f)?;
211 }
212 Ok(())
213 }
214 }
215 }
216}
217
218#[cfg(feature = "std")]
219impl std::error::Error for ValidationError {}
220
221#[cfg(all(test, feature = "alloc"))]
222mod tests {
223 use super::{ValidateResult, ValidationError, Violation};
224 use alloc::string::ToString;
225
226 #[test]
227 fn violation_new() {
228 let v = Violation::new("must not be empty");
229 assert_eq!(v.field, None);
230 assert_eq!(v.message, "must not be empty");
231 }
232
233 #[test]
234 fn violation_with_field() {
235 let v = Violation::with_field("email", "invalid format");
236 assert_eq!(v.field, Some("email"));
237 assert_eq!(v.message, "invalid format");
238 }
239
240 #[test]
241 fn violation_display_no_field() {
242 assert_eq!(Violation::new("bad value").to_string(), "bad value");
243 }
244
245 #[test]
246 fn violation_display_with_field() {
247 assert_eq!(
248 Violation::with_field("age", "must be positive").to_string(),
249 "age: must be positive"
250 );
251 }
252
253 #[test]
254 fn validation_error_single_violation() {
255 let e = ValidationError::new("value is required");
256 assert_eq!(e.len(), 1);
257 assert!(!e.is_empty());
258 assert_eq!(e.to_string(), "value is required");
259 }
260
261 #[test]
262 fn validation_error_field() {
263 let e = ValidationError::field("name", "too short");
264 assert_eq!(e.violations()[0].field, Some("name"));
265 assert_eq!(e.to_string(), "name: too short");
266 }
267
268 #[test]
269 fn validation_error_empty() {
270 let e = ValidationError::empty();
271 assert!(e.is_empty());
272 assert_eq!(e.len(), 0);
273 assert_eq!(e.to_string(), "validation failed");
274 }
275
276 #[test]
277 fn validation_error_add_chaining() {
278 let e = ValidationError::empty()
279 .with(Violation::with_field("name", "too short"))
280 .with(Violation::with_field("email", "invalid format"));
281 assert_eq!(e.len(), 2);
282 }
283
284 #[test]
285 fn validation_error_push() {
286 let mut e = ValidationError::empty();
287 e.push(Violation::new("first"));
288 e.push(Violation::new("second"));
289 assert_eq!(e.len(), 2);
290 }
291
292 #[test]
293 fn validation_error_merge() {
294 let a = ValidationError::new("first");
295 let b = ValidationError::new("second");
296 let merged = a.merge(b);
297 assert_eq!(merged.len(), 2);
298 }
299
300 #[test]
301 fn validation_error_display_multiple() {
302 let e = ValidationError::empty()
303 .with(Violation::new("first error"))
304 .with(Violation::new("second error"));
305 assert_eq!(e.to_string(), "first error; second error");
306 }
307
308 #[test]
309 fn validation_error_from_violation() {
310 let e = ValidationError::from(Violation::new("bad"));
311 assert_eq!(e.len(), 1);
312 }
313
314 #[test]
315 fn validation_error_from_str() {
316 let e = ValidationError::from("bad input");
317 assert_eq!(e.violations()[0].message, "bad input");
318 }
319
320 #[test]
321 fn validate_result_type_alias() {
322 let ok: ValidateResult = Ok(());
323 let err: ValidateResult = Err(ValidationError::new("fail"));
324 assert!(ok.is_ok());
325 assert!(err.is_err());
326 }
327
328 #[test]
329 fn require_adds_only_on_false_condition() {
330 let e = ValidationError::empty()
331 .require(true, Violation::new("kept ok"))
332 .require(false, Violation::with_field("name", "must not be empty"))
333 .require_field(false, "age", "must be at least 18");
334 assert_eq!(e.len(), 2);
335 assert_eq!(e.violations()[0].field, Some("name"));
336 assert_eq!(e.violations()[1].field, Some("age"));
337 }
338
339 #[test]
340 fn finish_maps_empty_to_ok_and_nonempty_to_err() {
341 assert!(ValidationError::empty().finish().is_ok());
342 let all_passing = ValidationError::empty()
343 .require(true, Violation::new("a"))
344 .require(true, Violation::new("b"))
345 .finish();
346 assert!(all_passing.is_ok());
347
348 let failing = ValidationError::empty()
349 .require_field(false, "x", "bad")
350 .finish();
351 assert_eq!(failing.unwrap_err().len(), 1);
352 }
353
354 #[test]
355 fn from_iter_and_extend_collect_violations() {
356 let e: ValidationError = [
357 Violation::new("first"),
358 Violation::with_field("name", "second"),
359 ]
360 .into_iter()
361 .collect();
362 assert_eq!(e.len(), 2);
363
364 let mut acc = ValidationError::empty();
365 acc.extend([Violation::new("a"), Violation::new("b")]);
366 assert_eq!(acc.len(), 2);
367 }
368}