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    /// Returns all violations.
105    pub fn violations(&self) -> &[Violation] {
106        &self.violations
107    }
108
109    /// Returns `true` if there are no violations.
110    pub fn is_empty(&self) -> bool {
111        self.violations.is_empty()
112    }
113
114    /// Returns the number of violations.
115    pub fn len(&self) -> usize {
116        self.violations.len()
117    }
118}
119
120#[cfg(feature = "alloc")]
121impl From<Violation> for ValidationError {
122    fn from(v: Violation) -> Self {
123        Self {
124            violations: alloc::vec![v],
125        }
126    }
127}
128
129#[cfg(feature = "alloc")]
130impl From<&'static str> for ValidationError {
131    fn from(message: &'static str) -> Self {
132        Self::new(message)
133    }
134}
135
136#[cfg(feature = "alloc")]
137impl fmt::Display for ValidationError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self.violations.as_slice() {
140            [] => f.write_str("validation failed"),
141            [single] => fmt::Display::fmt(single, f),
142            violations => {
143                for (i, v) in violations.iter().enumerate() {
144                    if i > 0 {
145                        write!(f, "; ")?;
146                    }
147                    fmt::Display::fmt(v, f)?;
148                }
149                Ok(())
150            }
151        }
152    }
153}
154
155#[cfg(feature = "std")]
156impl std::error::Error for ValidationError {}
157
158#[cfg(all(test, feature = "alloc"))]
159mod tests {
160    use super::{ValidateResult, ValidationError, Violation};
161    use alloc::string::ToString;
162
163    #[test]
164    fn violation_new() {
165        let v = Violation::new("must not be empty");
166        assert_eq!(v.field, None);
167        assert_eq!(v.message, "must not be empty");
168    }
169
170    #[test]
171    fn violation_with_field() {
172        let v = Violation::with_field("email", "invalid format");
173        assert_eq!(v.field, Some("email"));
174        assert_eq!(v.message, "invalid format");
175    }
176
177    #[test]
178    fn violation_display_no_field() {
179        assert_eq!(Violation::new("bad value").to_string(), "bad value");
180    }
181
182    #[test]
183    fn violation_display_with_field() {
184        assert_eq!(
185            Violation::with_field("age", "must be positive").to_string(),
186            "age: must be positive"
187        );
188    }
189
190    #[test]
191    fn validation_error_single_violation() {
192        let e = ValidationError::new("value is required");
193        assert_eq!(e.len(), 1);
194        assert!(!e.is_empty());
195        assert_eq!(e.to_string(), "value is required");
196    }
197
198    #[test]
199    fn validation_error_field() {
200        let e = ValidationError::field("name", "too short");
201        assert_eq!(e.violations()[0].field, Some("name"));
202        assert_eq!(e.to_string(), "name: too short");
203    }
204
205    #[test]
206    fn validation_error_empty() {
207        let e = ValidationError::empty();
208        assert!(e.is_empty());
209        assert_eq!(e.len(), 0);
210        assert_eq!(e.to_string(), "validation failed");
211    }
212
213    #[test]
214    fn validation_error_add_chaining() {
215        let e = ValidationError::empty()
216            .with(Violation::with_field("name", "too short"))
217            .with(Violation::with_field("email", "invalid format"));
218        assert_eq!(e.len(), 2);
219    }
220
221    #[test]
222    fn validation_error_push() {
223        let mut e = ValidationError::empty();
224        e.push(Violation::new("first"));
225        e.push(Violation::new("second"));
226        assert_eq!(e.len(), 2);
227    }
228
229    #[test]
230    fn validation_error_merge() {
231        let a = ValidationError::new("first");
232        let b = ValidationError::new("second");
233        let merged = a.merge(b);
234        assert_eq!(merged.len(), 2);
235    }
236
237    #[test]
238    fn validation_error_display_multiple() {
239        let e = ValidationError::empty()
240            .with(Violation::new("first error"))
241            .with(Violation::new("second error"));
242        assert_eq!(e.to_string(), "first error; second error");
243    }
244
245    #[test]
246    fn validation_error_from_violation() {
247        let e = ValidationError::from(Violation::new("bad"));
248        assert_eq!(e.len(), 1);
249    }
250
251    #[test]
252    fn validation_error_from_str() {
253        let e = ValidationError::from("bad input");
254        assert_eq!(e.violations()[0].message, "bad input");
255    }
256
257    #[test]
258    fn validate_result_type_alias() {
259        let ok: ValidateResult = Ok(());
260        let err: ValidateResult = Err(ValidationError::new("fail"));
261        assert!(ok.is_ok());
262        assert!(err.is_err());
263    }
264}