Skip to main content

vouched_core/
error.rs

1use core::{error::Error as StdError, fmt};
2
3#[cfg(feature = "alloc")]
4use alloc::boxed::Box;
5#[cfg(feature = "alloc")]
6use core::ops::Deref;
7
8#[cfg(feature = "valuable")]
9use alloc::string::ToString;
10#[cfg(feature = "valuable")]
11use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
12
13/// Error returned when a string newtype is shorter than its `len` lower bound.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TooShortError {
16    min: usize,
17    actual: usize,
18}
19
20impl TooShortError {
21    /// Creates a too-short error.
22    pub const fn new(min: usize, actual: usize) -> Self {
23        Self { min, actual }
24    }
25
26    /// Returns the minimum accepted length, measured as untrimmed Unicode scalar values.
27    pub const fn min(&self) -> usize {
28        self.min
29    }
30
31    /// Returns the actual length, measured as untrimmed Unicode scalar values.
32    pub const fn actual(&self) -> usize {
33        self.actual
34    }
35}
36
37impl fmt::Display for TooShortError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(
40            f,
41            "is too short (min {} chars, got {})",
42            self.min, self.actual
43        )
44    }
45}
46
47impl StdError for TooShortError {}
48
49/// Error returned when a string newtype is longer than its `len` upper bound.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct TooLongError {
52    max: usize,
53    actual: usize,
54}
55
56impl TooLongError {
57    /// Creates a too-long error.
58    pub const fn new(max: usize, actual: usize) -> Self {
59        Self { max, actual }
60    }
61
62    /// Returns the maximum accepted length, measured as untrimmed Unicode scalar values.
63    pub const fn max(&self) -> usize {
64        self.max
65    }
66
67    /// Returns the actual length, measured as untrimmed Unicode scalar values.
68    pub const fn actual(&self) -> usize {
69        self.actual
70    }
71}
72
73impl fmt::Display for TooLongError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(
76            f,
77            "is too long (max {} chars, got {})",
78            self.max, self.actual
79        )
80    }
81}
82
83impl StdError for TooLongError {}
84
85#[derive(Debug, Copy, Clone, PartialEq, Eq)]
86enum NumericValueRepr {
87    Signed(i128),
88    Unsigned(u128),
89}
90
91/// Lossless numeric value captured by generated range and cast errors.
92///
93/// The representation is private so future integer-like values can be added
94/// without exposing the enum shape. Use the `as_*` methods to recover a
95/// primitive integer only when the conversion is lossless.
96#[derive(Debug, Copy, Clone, PartialEq, Eq)]
97pub struct NumericValue(NumericValueRepr);
98
99impl NumericValue {
100    /// Captures a signed integer value.
101    pub const fn from_i128(value: i128) -> Self {
102        Self(NumericValueRepr::Signed(value))
103    }
104
105    /// Captures an unsigned integer value.
106    pub const fn from_u128(value: u128) -> Self {
107        Self(NumericValueRepr::Unsigned(value))
108    }
109
110    /// Returns the value as `i64` when it fits exactly.
111    pub fn as_i64(self) -> Option<i64> {
112        match self.0 {
113            NumericValueRepr::Signed(value) => i64::try_from(value).ok(),
114            NumericValueRepr::Unsigned(value) => i64::try_from(value).ok(),
115        }
116    }
117
118    /// Returns the value as `u64` when it fits exactly.
119    pub fn as_u64(self) -> Option<u64> {
120        match self.0 {
121            NumericValueRepr::Signed(value) => u64::try_from(value).ok(),
122            NumericValueRepr::Unsigned(value) => u64::try_from(value).ok(),
123        }
124    }
125
126    /// Returns the value as `i128` when it fits exactly.
127    pub fn as_i128(self) -> Option<i128> {
128        match self.0 {
129            NumericValueRepr::Signed(value) => Some(value),
130            NumericValueRepr::Unsigned(value) => i128::try_from(value).ok(),
131        }
132    }
133
134    /// Returns the value as `u128` when it fits exactly.
135    pub fn as_u128(self) -> Option<u128> {
136        match self.0 {
137            NumericValueRepr::Signed(value) => u128::try_from(value).ok(),
138            NumericValueRepr::Unsigned(value) => Some(value),
139        }
140    }
141}
142
143impl fmt::Display for NumericValue {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self.0 {
146            NumericValueRepr::Signed(value) => write!(f, "{value}"),
147            NumericValueRepr::Unsigned(value) => write!(f, "{value}"),
148        }
149    }
150}
151
152macro_rules! impl_signed_numeric_value_from {
153    ($($ty:ty),* $(,)?) => {
154        $(
155            impl From<$ty> for NumericValue {
156                fn from(value: $ty) -> Self {
157                    Self::from_i128(i128::from(value))
158                }
159            }
160        )*
161    };
162}
163
164macro_rules! impl_unsigned_numeric_value_from {
165    ($($ty:ty),* $(,)?) => {
166        $(
167            impl From<$ty> for NumericValue {
168                fn from(value: $ty) -> Self {
169                    Self::from_u128(u128::from(value))
170                }
171            }
172        )*
173    };
174}
175
176impl_signed_numeric_value_from!(i8, i16, i32, i64, i128);
177impl_unsigned_numeric_value_from!(u8, u16, u32, u64, u128);
178
179#[cfg(feature = "valuable")]
180impl Valuable for NumericValue {
181    fn as_value(&self) -> Value<'_> {
182        match self.0 {
183            NumericValueRepr::Signed(value) => Value::I128(value),
184            NumericValueRepr::Unsigned(value) => Value::U128(value),
185        }
186    }
187
188    fn visit(&self, visit: &mut dyn Visit) {
189        visit.visit_value(self.as_value());
190    }
191}
192
193/// Error returned when a numeric newtype is outside its `range` bounds.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct OutOfRangeNumericError {
196    actual: NumericValue,
197    lower_bound: Option<NumericValue>,
198    upper_bound: Option<NumericValue>,
199}
200
201impl OutOfRangeNumericError {
202    /// Creates an out-of-range error with the value that failed validation.
203    pub const fn new(actual: NumericValue) -> Self {
204        Self {
205            actual,
206            lower_bound: None,
207            upper_bound: None,
208        }
209    }
210
211    /// Returns an out-of-range error with the lower bound that failed.
212    #[must_use]
213    pub const fn with_lower_bound(self, lower_bound: NumericValue) -> Self {
214        Self {
215            actual: self.actual,
216            lower_bound: Some(lower_bound),
217            upper_bound: self.upper_bound,
218        }
219    }
220
221    /// Returns an out-of-range error with the upper bound that failed.
222    #[must_use]
223    pub const fn with_upper_bound(self, upper_bound: NumericValue) -> Self {
224        Self {
225            actual: self.actual,
226            lower_bound: self.lower_bound,
227            upper_bound: Some(upper_bound),
228        }
229    }
230
231    /// Returns the actual value that failed validation.
232    pub const fn actual(&self) -> NumericValue {
233        self.actual
234    }
235
236    /// Returns the lower bound that failed when it could be captured losslessly.
237    pub const fn lower_bound(&self) -> Option<NumericValue> {
238        self.lower_bound
239    }
240
241    /// Returns the upper bound that failed when it could be captured losslessly.
242    pub const fn upper_bound(&self) -> Option<NumericValue> {
243        self.upper_bound
244    }
245}
246
247impl fmt::Display for OutOfRangeNumericError {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        let actual = self.actual;
250        match (self.lower_bound, self.upper_bound) {
251            (Some(lower_bound), Some(upper_bound)) => {
252                write!(
253                    f,
254                    "out of range (value {actual}, lower bound {lower_bound}, upper bound {upper_bound})"
255                )
256            }
257            (Some(lower_bound), None) => {
258                write!(
259                    f,
260                    "out of range (value {actual}, lower bound {lower_bound})"
261                )
262            }
263            (None, Some(upper_bound)) => {
264                write!(
265                    f,
266                    "out of range (value {actual}, upper bound {upper_bound})"
267                )
268            }
269            (None, None) => write!(f, "out of range (value {actual})"),
270        }
271    }
272}
273
274impl StdError for OutOfRangeNumericError {}
275
276#[cfg(feature = "valuable")]
277static OUT_OF_RANGE_NUMERIC_ERROR_FIELDS: &[NamedField<'static>] = &[
278    NamedField::new("actual"),
279    NamedField::new("lower_bound"),
280    NamedField::new("upper_bound"),
281];
282
283#[cfg(feature = "valuable")]
284impl Valuable for OutOfRangeNumericError {
285    fn as_value(&self) -> Value<'_> {
286        Value::Structable(self)
287    }
288
289    fn visit(&self, visit: &mut dyn Visit) {
290        let values = [
291            self.actual.as_value(),
292            self.lower_bound.as_value(),
293            self.upper_bound.as_value(),
294        ];
295        visit.visit_named_fields(&NamedValues::new(
296            OUT_OF_RANGE_NUMERIC_ERROR_FIELDS,
297            &values,
298        ));
299    }
300}
301
302#[cfg(feature = "valuable")]
303impl Structable for OutOfRangeNumericError {
304    fn definition(&self) -> StructDef<'_> {
305        StructDef::new_static(
306            "OutOfRangeNumericError",
307            Fields::Named(OUT_OF_RANGE_NUMERIC_ERROR_FIELDS),
308        )
309    }
310}
311
312/// Error returned when a string newtype contains a character rejected by `chars`.
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct InvalidCharError {
315    index: usize,
316    ch: char,
317}
318
319impl InvalidCharError {
320    /// Creates an invalid-character error.
321    pub const fn new(index: usize, ch: char) -> Self {
322        Self { index, ch }
323    }
324
325    /// Returns the zero-based character index, not a byte offset.
326    pub const fn index(&self) -> usize {
327        self.index
328    }
329
330    /// Returns the rejected character.
331    pub const fn ch(&self) -> char {
332        self.ch
333    }
334}
335
336impl fmt::Display for InvalidCharError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        write!(
339            f,
340            "contains invalid character '{}' at index {}",
341            self.ch, self.index
342        )
343    }
344}
345
346impl StdError for InvalidCharError {}
347
348/// Common interface for generated Vouched errors.
349///
350/// Error types generated by the Vouched derive macro implement this trait.
351/// The `as_*` methods provide type-safe access to specific validation errors.
352pub trait VouchedError: StdError + Send + Sync + 'static {
353    /// Returns the underlying too-short error when this is that variant.
354    fn as_too_short(&self) -> Option<&TooShortError> {
355        None
356    }
357    /// Returns the underlying too-long error when this is that variant.
358    fn as_too_long(&self) -> Option<&TooLongError> {
359        None
360    }
361    /// Returns the underlying numeric out-of-range error when this is that variant.
362    fn as_out_of_range_numeric(&self) -> Option<&OutOfRangeNumericError> {
363        None
364    }
365    /// Returns the underlying invalid-character error when this is that variant.
366    fn as_invalid_char(&self) -> Option<&InvalidCharError> {
367        None
368    }
369}
370
371/// Wrapper type for handling different Vouched error types uniformly.
372///
373/// This allows code that works with multiple Vouched wrappers to handle their
374/// validation errors through a single type. It supports automatic conversion
375/// through the `?` operator.
376///
377/// Available with the `alloc` feature, which is enabled by the default `std`
378/// feature.
379///
380/// # Examples
381///
382/// ```
383/// # use vouched_core::*;
384/// #
385/// # #[derive(Debug)]
386/// # enum UserIdError { TooShort(TooShortError), TooLong(TooLongError) }
387/// # impl core::fmt::Display for UserIdError {
388/// #     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
389/// #         match self { UserIdError::TooShort(e) => e.fmt(f), UserIdError::TooLong(e) => e.fmt(f) }
390/// #     }
391/// # }
392/// # impl core::error::Error for UserIdError {}
393/// # impl VouchedError for UserIdError {
394/// #     fn as_too_short(&self) -> Option<&TooShortError> {
395/// #         match self { UserIdError::TooShort(e) => Some(e), _ => None }
396/// #     }
397/// #     fn as_too_long(&self) -> Option<&TooLongError> {
398/// #         match self { UserIdError::TooLong(e) => Some(e), _ => None }
399/// #     }
400/// # }
401/// #
402/// // Function that handles different Vouched error types uniformly.
403/// fn process_errors() -> Result<(), Error> {
404///     // Any error implementing VouchedError can be converted into Error.
405///     let err = UserIdError::TooShort(TooShortError::new(1, 0));
406///     Err(Error::from(err))
407/// }
408///
409/// // Inspect the concrete validation error kind.
410/// let result = process_errors();
411/// assert!(result.is_err());
412/// let err = result.unwrap_err();
413/// assert!(err.as_too_short().is_some());
414/// ```
415#[cfg(feature = "alloc")]
416#[derive(Debug)]
417pub struct Error(Box<dyn VouchedError>);
418
419#[cfg(feature = "alloc")]
420impl Error {
421    /// Wraps a boxed generated Vouched error.
422    pub fn new(inner: Box<dyn VouchedError>) -> Self {
423        Self(inner)
424    }
425
426    /// Returns the wrapped generated Vouched error.
427    pub fn into_inner(self) -> Box<dyn VouchedError> {
428        self.0
429    }
430}
431
432#[cfg(feature = "alloc")]
433impl fmt::Display for Error {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        self.0.fmt(f)
436    }
437}
438
439#[cfg(feature = "alloc")]
440impl StdError for Error {
441    fn source(&self) -> Option<&(dyn StdError + 'static)> {
442        Some(&*self.0)
443    }
444}
445
446#[cfg(feature = "alloc")]
447impl<E> From<E> for Error
448where
449    E: VouchedError,
450{
451    fn from(e: E) -> Self {
452        Self(Box::new(e))
453    }
454}
455
456#[cfg(feature = "alloc")]
457impl Deref for Error {
458    type Target = dyn VouchedError;
459
460    fn deref(&self) -> &Self::Target {
461        &*self.0
462    }
463}
464
465#[cfg(feature = "alloc")]
466impl AsRef<dyn VouchedError> for Error {
467    fn as_ref(&self) -> &(dyn VouchedError + 'static) {
468        &*self.0
469    }
470}
471
472#[cfg(feature = "valuable")]
473static ERROR_FIELDS: &[NamedField<'static>] = &[NamedField::new("message")];
474
475#[cfg(feature = "valuable")]
476impl Valuable for Error {
477    fn as_value(&self) -> Value<'_> {
478        Value::Structable(self)
479    }
480
481    fn visit(&self, visit: &mut dyn Visit) {
482        let message = self.to_string();
483        let values = [message.as_value()];
484        visit.visit_named_fields(&NamedValues::new(ERROR_FIELDS, &values));
485    }
486}
487
488#[cfg(feature = "valuable")]
489impl Structable for Error {
490    fn definition(&self) -> StructDef<'_> {
491        StructDef::new_static("Error", Fields::Named(ERROR_FIELDS))
492    }
493}