Skip to main content

type_lib/
error.rs

1//! The crate's ready-made validation error.
2//!
3//! Most validators need to report only two things: *which* invariant failed and
4//! a short human-readable explanation. [`ValidationError`] carries exactly that,
5//! using two `'static` string slices so it allocates nothing and works under
6//! `no_std`. Validators that need richer, structured failures are free to define
7//! their own error type — [`Validator::Error`](crate::Validator::Error) is an
8//! associated type, not a fixed choice.
9
10use core::fmt;
11
12/// A lightweight validation failure with a machine-readable code and a
13/// human-readable message.
14///
15/// `ValidationError` is the default error returned by the simple validators in
16/// this crate and the recommended starting point for hand-written ones. It is
17/// `Copy`, holds no owned data, and never allocates, which keeps it usable on a
18/// hot path and in `no_std` builds alike.
19///
20/// The `code` is meant to be a stable, lowercase identifier you can match on
21/// (`"non_empty"`, `"out_of_range"`); the `message` is meant for logs and
22/// end-user diagnostics. Keep the code stable across releases even if you reword
23/// the message.
24///
25/// Under the `std` feature it implements `std::error::Error`, so it slots into
26/// `?` and `Box<dyn Error>` chains without ceremony.
27///
28/// # Examples
29///
30/// Construct one directly and inspect its parts:
31///
32/// ```rust
33/// use type_lib::ValidationError;
34///
35/// let err = ValidationError::new("non_empty", "value must not be empty");
36/// assert_eq!(err.code(), "non_empty");
37/// assert_eq!(err.message(), "value must not be empty");
38/// ```
39///
40/// Branch on the stable code while showing the message to a human:
41///
42/// ```rust
43/// use type_lib::ValidationError;
44///
45/// fn describe(err: &ValidationError) -> &'static str {
46///     match err.code() {
47///         "non_empty" => "the field was left blank",
48///         "out_of_range" => "the number is outside the allowed range",
49///         _ => "the value is invalid",
50///     }
51/// }
52///
53/// let err = ValidationError::new("out_of_range", "expected 1..=10, got 42");
54/// assert_eq!(describe(&err), "the number is outside the allowed range");
55/// ```
56///
57/// Format it for display (the `Display` output is `"<code>: <message>"`):
58///
59/// ```rust
60/// use type_lib::ValidationError;
61///
62/// let err = ValidationError::new("non_empty", "value must not be empty");
63/// assert_eq!(err.to_string(), "non_empty: value must not be empty");
64/// ```
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct ValidationError {
67    code: &'static str,
68    message: &'static str,
69}
70
71impl ValidationError {
72    /// Creates a validation error from a stable `code` and a human-readable
73    /// `message`.
74    ///
75    /// This is a `const fn`, so errors can be declared as associated constants
76    /// or `static`s and reused without per-call construction cost.
77    ///
78    /// # Examples
79    ///
80    /// ```rust
81    /// use type_lib::ValidationError;
82    ///
83    /// const EMPTY: ValidationError =
84    ///     ValidationError::new("non_empty", "value must not be empty");
85    /// assert_eq!(EMPTY.code(), "non_empty");
86    /// ```
87    #[must_use]
88    pub const fn new(code: &'static str, message: &'static str) -> Self {
89        Self { code, message }
90    }
91
92    /// Returns the stable, machine-readable error code.
93    ///
94    /// Use this to branch programmatically; it is intended to stay stable across
95    /// releases even when the message changes.
96    ///
97    /// # Examples
98    ///
99    /// ```rust
100    /// use type_lib::ValidationError;
101    ///
102    /// let err = ValidationError::new("non_empty", "value must not be empty");
103    /// assert_eq!(err.code(), "non_empty");
104    /// ```
105    #[must_use]
106    pub const fn code(&self) -> &'static str {
107        self.code
108    }
109
110    /// Returns the human-readable message.
111    ///
112    /// Intended for logs and diagnostics, not for matching; the wording may
113    /// change between releases.
114    ///
115    /// # Examples
116    ///
117    /// ```rust
118    /// use type_lib::ValidationError;
119    ///
120    /// let err = ValidationError::new("non_empty", "value must not be empty");
121    /// assert_eq!(err.message(), "value must not be empty");
122    /// ```
123    #[must_use]
124    pub const fn message(&self) -> &'static str {
125        self.message
126    }
127}
128
129impl fmt::Display for ValidationError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "{}: {}", self.code, self.message)
132    }
133}
134
135#[cfg(feature = "std")]
136impl std::error::Error for ValidationError {}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn new_preserves_code_and_message() {
144        let err = ValidationError::new("non_empty", "value must not be empty");
145        assert_eq!(err.code(), "non_empty");
146        assert_eq!(err.message(), "value must not be empty");
147    }
148
149    #[test]
150    fn display_joins_code_and_message() {
151        // Format through `core::fmt::Write` into a fixed buffer so the test
152        // holds under `no_std` as well as `std`.
153        use core::fmt::Write as _;
154
155        let err = ValidationError::new("out_of_range", "expected 1..=10");
156        let mut buf = Buf::new();
157        let _ = write!(buf, "{err}");
158        assert_eq!(buf.as_str(), "out_of_range: expected 1..=10");
159    }
160
161    #[test]
162    fn equal_errors_compare_equal() {
163        let a = ValidationError::new("c", "m");
164        let b = ValidationError::new("c", "m");
165        let c = ValidationError::new("c", "other");
166        assert_eq!(a, b);
167        assert_ne!(a, c);
168    }
169
170    /// A tiny fixed-capacity formatter sink, so the `Display` test needs neither
171    /// `alloc` nor `std`. It stores up to 64 bytes; the message under test is
172    /// shorter than that.
173    struct Buf {
174        bytes: [u8; 64],
175        len: usize,
176    }
177
178    impl Buf {
179        fn new() -> Self {
180            Self {
181                bytes: [0; 64],
182                len: 0,
183            }
184        }
185
186        fn as_str(&self) -> &str {
187            core::str::from_utf8(&self.bytes[..self.len]).unwrap_or("")
188        }
189    }
190
191    impl core::fmt::Write for Buf {
192        fn write_str(&mut self, s: &str) -> core::fmt::Result {
193            for &byte in s.as_bytes() {
194                if self.len < self.bytes.len() {
195                    self.bytes[self.len] = byte;
196                    self.len += 1;
197                }
198            }
199            Ok(())
200        }
201    }
202}