Skip to main content

reliakit_validate/
error.rs

1#[cfg(feature = "alloc")]
2use alloc::vec::Vec;
3use core::fmt;
4
5/// A single failed constraint, optionally associated with a named field.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub struct Violation {
9    /// The field name, if validation was run on a named field.
10    pub field: Option<&'static str>,
11    /// A human-readable description of the constraint that failed.
12    pub message: &'static str,
13}
14
15impl Violation {
16    /// Creates a violation without a field name.
17    pub const fn new(message: &'static str) -> Self {
18        Self {
19            field: None,
20            message,
21        }
22    }
23
24    /// Creates a violation associated with a named field.
25    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/// One or more validation failures collected during validation.
43///
44/// `ValidationError` is designed for multi-field struct validation where all
45/// fields should be checked and all violations reported together. For
46/// single-value validation, a simpler error type may be more appropriate.
47///
48/// Requires the `alloc` feature (enabled by default via `std`), since it is
49/// backed by `Vec<Violation>`.
50#[cfg(feature = "alloc")]
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ValidationError {
53    violations: Vec<Violation>,
54}
55
56/// Result alias for validation operations.
57#[cfg(feature = "alloc")]
58pub type ValidateResult<T = ()> = Result<T, ValidationError>;
59
60#[cfg(feature = "alloc")]
61impl ValidationError {
62    /// Creates a `ValidationError` with a single unnamed violation.
63    pub fn new(message: &'static str) -> Self {
64        Self {
65            violations: alloc::vec![Violation::new(message)],
66        }
67    }
68
69    /// Creates a `ValidationError` with a single named field violation.
70    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    /// Creates an empty `ValidationError`. Useful for building up violations.
77    ///
78    /// Always check [`is_empty`](Self::is_empty) before returning this as
79    /// `Err`. Returning an empty `ValidationError` is valid Rust but conveys
80    /// no information to the caller.
81    pub fn empty() -> Self {
82        Self {
83            violations: Vec::new(),
84        }
85    }
86
87    /// Adds a violation and returns `self` for chaining.
88    pub fn with(mut self, violation: Violation) -> Self {
89        self.violations.push(violation);
90        self
91    }
92
93    /// Adds a violation in place.
94    pub fn push(&mut self, violation: Violation) {
95        self.violations.push(violation);
96    }
97
98    /// Merges another `ValidationError` into this one.
99    pub fn merge(mut self, other: Self) -> Self {
100        self.violations.extend(other.violations);
101        self
102    }
103
104    /// Records `violation` only when `condition` is `false`, and returns `self`
105    /// for chaining. A `true` condition means the rule held, so nothing is added.
106    ///
107    /// Pair this with [`finish`](Self::finish) to express multi-field validation
108    /// without the easy-to-forget final emptiness check:
109    ///
110    /// ```
111    /// use reliakit_validate::{ValidationError, Violation};
112    ///
113    /// let result = ValidationError::empty()
114    ///     .require(!"".is_empty(), Violation::with_field("name", "must not be empty"))
115    ///     .require(15 >= 18, Violation::with_field("age", "must be at least 18"))
116    ///     .finish();
117    ///
118    /// assert_eq!(result.unwrap_err().len(), 2);
119    /// ```
120    pub fn require(mut self, condition: bool, violation: Violation) -> Self {
121        if !condition {
122            self.violations.push(violation);
123        }
124        self
125    }
126
127    /// Like [`require`](Self::require), building the [`Violation`] from a field
128    /// name and message.
129    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    /// Converts the accumulated violations into a result: `Ok(())` when there
139    /// are none, otherwise `Err(self)`.
140    ///
141    /// This removes the footgun of returning an empty `ValidationError` as an
142    /// error (see [`empty`](Self::empty)).
143    pub fn finish(self) -> ValidateResult {
144        if self.is_empty() {
145            Ok(())
146        } else {
147            Err(self)
148        }
149    }
150
151    /// Returns all violations.
152    pub fn violations(&self) -> &[Violation] {
153        &self.violations
154    }
155
156    /// Returns `true` if there are no violations.
157    pub fn is_empty(&self) -> bool {
158        self.violations.is_empty()
159    }
160
161    /// Returns the number of violations.
162    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}