Skip to main content

reliakit_validate/
error.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4/// A single failed constraint, optionally associated with a named field.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Violation {
7    /// The field name, if validation was run on a named field.
8    pub field: Option<&'static str>,
9    /// A human-readable description of the constraint that failed.
10    pub message: &'static str,
11}
12
13impl Violation {
14    /// Creates a violation without a field name.
15    pub const fn new(message: &'static str) -> Self {
16        Self {
17            field: None,
18            message,
19        }
20    }
21
22    /// Creates a violation associated with a named field.
23    pub const fn with_field(field: &'static str, message: &'static str) -> Self {
24        Self {
25            field: Some(field),
26            message,
27        }
28    }
29}
30
31impl fmt::Display for Violation {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self.field {
34            Some(field) => write!(f, "{field}: {}", self.message),
35            None => f.write_str(self.message),
36        }
37    }
38}
39
40/// One or more validation failures collected during validation.
41///
42/// `ValidationError` is designed for multi-field struct validation where all
43/// fields should be checked and all violations reported together. For
44/// single-value validation, a simpler error type may be more appropriate.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ValidationError {
47    violations: Vec<Violation>,
48}
49
50/// Result alias for validation operations.
51pub type ValidateResult<T = ()> = Result<T, ValidationError>;
52
53impl ValidationError {
54    /// Creates a `ValidationError` with a single unnamed violation.
55    pub fn new(message: &'static str) -> Self {
56        Self {
57            violations: alloc::vec![Violation::new(message)],
58        }
59    }
60
61    /// Creates a `ValidationError` with a single named field violation.
62    pub fn field(field: &'static str, message: &'static str) -> Self {
63        Self {
64            violations: alloc::vec![Violation::with_field(field, message)],
65        }
66    }
67
68    /// Creates an empty `ValidationError`. Useful for building up violations.
69    ///
70    /// Always check [`is_empty`](Self::is_empty) before returning this as
71    /// `Err`. Returning an empty `ValidationError` is valid Rust but conveys
72    /// no information to the caller.
73    pub fn empty() -> Self {
74        Self {
75            violations: Vec::new(),
76        }
77    }
78
79    /// Adds a violation and returns `self` for chaining.
80    pub fn with(mut self, violation: Violation) -> Self {
81        self.violations.push(violation);
82        self
83    }
84
85    /// Adds a violation in place.
86    pub fn push(&mut self, violation: Violation) {
87        self.violations.push(violation);
88    }
89
90    /// Merges another `ValidationError` into this one.
91    pub fn merge(mut self, other: Self) -> Self {
92        self.violations.extend(other.violations);
93        self
94    }
95
96    /// Returns all violations.
97    pub fn violations(&self) -> &[Violation] {
98        &self.violations
99    }
100
101    /// Returns `true` if there are no violations.
102    pub fn is_empty(&self) -> bool {
103        self.violations.is_empty()
104    }
105
106    /// Returns the number of violations.
107    pub fn len(&self) -> usize {
108        self.violations.len()
109    }
110}
111
112impl From<Violation> for ValidationError {
113    fn from(v: Violation) -> Self {
114        Self {
115            violations: alloc::vec![v],
116        }
117    }
118}
119
120impl From<&'static str> for ValidationError {
121    fn from(message: &'static str) -> Self {
122        Self::new(message)
123    }
124}
125
126impl fmt::Display for ValidationError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self.violations.as_slice() {
129            [] => f.write_str("validation failed"),
130            [single] => fmt::Display::fmt(single, f),
131            violations => {
132                for (i, v) in violations.iter().enumerate() {
133                    if i > 0 {
134                        write!(f, "; ")?;
135                    }
136                    fmt::Display::fmt(v, f)?;
137                }
138                Ok(())
139            }
140        }
141    }
142}
143
144#[cfg(feature = "std")]
145impl std::error::Error for ValidationError {}
146
147#[cfg(test)]
148mod tests {
149    use super::{ValidateResult, ValidationError, Violation};
150    use alloc::string::ToString;
151
152    #[test]
153    fn violation_new() {
154        let v = Violation::new("must not be empty");
155        assert_eq!(v.field, None);
156        assert_eq!(v.message, "must not be empty");
157    }
158
159    #[test]
160    fn violation_with_field() {
161        let v = Violation::with_field("email", "invalid format");
162        assert_eq!(v.field, Some("email"));
163        assert_eq!(v.message, "invalid format");
164    }
165
166    #[test]
167    fn violation_display_no_field() {
168        assert_eq!(Violation::new("bad value").to_string(), "bad value");
169    }
170
171    #[test]
172    fn violation_display_with_field() {
173        assert_eq!(
174            Violation::with_field("age", "must be positive").to_string(),
175            "age: must be positive"
176        );
177    }
178
179    #[test]
180    fn validation_error_single_violation() {
181        let e = ValidationError::new("value is required");
182        assert_eq!(e.len(), 1);
183        assert!(!e.is_empty());
184        assert_eq!(e.to_string(), "value is required");
185    }
186
187    #[test]
188    fn validation_error_field() {
189        let e = ValidationError::field("name", "too short");
190        assert_eq!(e.violations()[0].field, Some("name"));
191        assert_eq!(e.to_string(), "name: too short");
192    }
193
194    #[test]
195    fn validation_error_empty() {
196        let e = ValidationError::empty();
197        assert!(e.is_empty());
198        assert_eq!(e.len(), 0);
199        assert_eq!(e.to_string(), "validation failed");
200    }
201
202    #[test]
203    fn validation_error_add_chaining() {
204        let e = ValidationError::empty()
205            .with(Violation::with_field("name", "too short"))
206            .with(Violation::with_field("email", "invalid format"));
207        assert_eq!(e.len(), 2);
208    }
209
210    #[test]
211    fn validation_error_push() {
212        let mut e = ValidationError::empty();
213        e.push(Violation::new("first"));
214        e.push(Violation::new("second"));
215        assert_eq!(e.len(), 2);
216    }
217
218    #[test]
219    fn validation_error_merge() {
220        let a = ValidationError::new("first");
221        let b = ValidationError::new("second");
222        let merged = a.merge(b);
223        assert_eq!(merged.len(), 2);
224    }
225
226    #[test]
227    fn validation_error_display_multiple() {
228        let e = ValidationError::empty()
229            .with(Violation::new("first error"))
230            .with(Violation::new("second error"));
231        assert_eq!(e.to_string(), "first error; second error");
232    }
233
234    #[test]
235    fn validation_error_from_violation() {
236        let e = ValidationError::from(Violation::new("bad"));
237        assert_eq!(e.len(), 1);
238    }
239
240    #[test]
241    fn validation_error_from_str() {
242        let e = ValidationError::from("bad input");
243        assert_eq!(e.violations()[0].message, "bad input");
244    }
245
246    #[test]
247    fn validate_result_type_alias() {
248        let ok: ValidateResult = Ok(());
249        let err: ValidateResult = Err(ValidationError::new("fail"));
250        assert!(ok.is_ok());
251        assert!(err.is_err());
252    }
253}