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