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}