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 IntegerValueRepr {
87    Signed(i128),
88    Unsigned(u128),
89}
90
91/// Lossless integer value captured by generated integer range and conversion 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 IntegerValue(IntegerValueRepr);
98
99impl IntegerValue {
100    /// Captures a signed integer value.
101    pub const fn from_i128(value: i128) -> Self {
102        Self(IntegerValueRepr::Signed(value))
103    }
104
105    /// Captures an unsigned integer value.
106    pub const fn from_u128(value: u128) -> Self {
107        Self(IntegerValueRepr::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            IntegerValueRepr::Signed(value) => i64::try_from(value).ok(),
114            IntegerValueRepr::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            IntegerValueRepr::Signed(value) => u64::try_from(value).ok(),
122            IntegerValueRepr::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            IntegerValueRepr::Signed(value) => Some(value),
130            IntegerValueRepr::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            IntegerValueRepr::Signed(value) => u128::try_from(value).ok(),
138            IntegerValueRepr::Unsigned(value) => Some(value),
139        }
140    }
141}
142
143impl fmt::Display for IntegerValue {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self.0 {
146            IntegerValueRepr::Signed(value) => write!(f, "{value}"),
147            IntegerValueRepr::Unsigned(value) => write!(f, "{value}"),
148        }
149    }
150}
151
152macro_rules! impl_signed_integer_value_from {
153    ($($ty:ty),* $(,)?) => {
154        $(
155            impl From<$ty> for IntegerValue {
156                fn from(value: $ty) -> Self {
157                    Self::from_i128(i128::from(value))
158                }
159            }
160        )*
161    };
162}
163
164macro_rules! impl_unsigned_integer_value_from {
165    ($($ty:ty),* $(,)?) => {
166        $(
167            impl From<$ty> for IntegerValue {
168                fn from(value: $ty) -> Self {
169                    Self::from_u128(u128::from(value))
170                }
171            }
172        )*
173    };
174}
175
176impl_signed_integer_value_from!(i8, i16, i32, i64, i128);
177impl_unsigned_integer_value_from!(u8, u16, u32, u64, u128);
178
179#[cfg(feature = "valuable")]
180impl Valuable for IntegerValue {
181    fn as_value(&self) -> Value<'_> {
182        match self.0 {
183            IntegerValueRepr::Signed(value) => Value::I128(value),
184            IntegerValueRepr::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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
194enum FloatValueRepr {
195    F32(u32),
196    F64(u64),
197}
198
199/// Lossless float value captured by generated float range errors.
200///
201/// The value stores the original `to_bits()` representation so `NaN` payloads
202/// and signed zero can be preserved while keeping equality total.
203#[derive(Debug, Copy, Clone, PartialEq, Eq)]
204pub struct FloatValue(FloatValueRepr);
205
206impl FloatValue {
207    /// Captures an `f32` value by its bit representation.
208    pub const fn from_f32(value: f32) -> Self {
209        Self(FloatValueRepr::F32(value.to_bits()))
210    }
211
212    /// Captures an `f64` value by its bit representation.
213    pub const fn from_f64(value: f64) -> Self {
214        Self(FloatValueRepr::F64(value.to_bits()))
215    }
216
217    /// Returns the captured `f32` value when this value came from `f32`.
218    pub const fn as_f32(self) -> Option<f32> {
219        match self.0 {
220            FloatValueRepr::F32(bits) => Some(f32::from_bits(bits)),
221            FloatValueRepr::F64(_) => None,
222        }
223    }
224
225    /// Returns the captured `f64` value when this value came from `f64`.
226    pub const fn as_f64(self) -> Option<f64> {
227        match self.0 {
228            FloatValueRepr::F32(_) => None,
229            FloatValueRepr::F64(bits) => Some(f64::from_bits(bits)),
230        }
231    }
232}
233
234impl From<f32> for FloatValue {
235    fn from(value: f32) -> Self {
236        Self::from_f32(value)
237    }
238}
239
240impl From<f64> for FloatValue {
241    fn from(value: f64) -> Self {
242        Self::from_f64(value)
243    }
244}
245
246impl fmt::Display for FloatValue {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self.0 {
249            FloatValueRepr::F32(bits) => {
250                let value = f32::from_bits(bits);
251                write!(f, "{value}")
252            }
253            FloatValueRepr::F64(bits) => {
254                let value = f64::from_bits(bits);
255                write!(f, "{value}")
256            }
257        }
258    }
259}
260
261#[cfg(feature = "valuable")]
262impl Valuable for FloatValue {
263    fn as_value(&self) -> Value<'_> {
264        match self.0 {
265            FloatValueRepr::F32(bits) => Value::F32(f32::from_bits(bits)),
266            FloatValueRepr::F64(bits) => Value::F64(f64::from_bits(bits)),
267        }
268    }
269
270    fn visit(&self, visit: &mut dyn Visit) {
271        visit.visit_value(self.as_value());
272    }
273}
274
275/// Reason a float range validation failed.
276#[derive(Debug, Copy, Clone, PartialEq, Eq)]
277pub enum FloatRangeViolation {
278    /// The value could not be compared to the configured bounds, such as `NaN`.
279    NotComparable,
280    /// The value was below the lower bound.
281    BelowLowerBound,
282    /// The value was above the upper bound.
283    AboveUpperBound,
284}
285
286impl FloatRangeViolation {
287    /// Returns the stable observation string for this violation.
288    pub const fn as_str(self) -> &'static str {
289        match self {
290            Self::NotComparable => "not_comparable",
291            Self::BelowLowerBound => "below_lower_bound",
292            Self::AboveUpperBound => "above_upper_bound",
293        }
294    }
295}
296
297#[cfg(feature = "valuable")]
298impl Valuable for FloatRangeViolation {
299    fn as_value(&self) -> Value<'_> {
300        Value::String(self.as_str())
301    }
302
303    fn visit(&self, visit: &mut dyn Visit) {
304        visit.visit_value(self.as_value());
305    }
306}
307
308/// Error returned when an integer newtype is outside its `range` bounds.
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct OutOfRangeIntegerError {
311    actual: IntegerValue,
312    lower_bound: Option<IntegerValue>,
313    upper_bound: Option<IntegerValue>,
314}
315
316impl OutOfRangeIntegerError {
317    /// Creates an out-of-range error with the value that failed validation.
318    pub const fn new(actual: IntegerValue) -> Self {
319        Self {
320            actual,
321            lower_bound: None,
322            upper_bound: None,
323        }
324    }
325
326    /// Returns an out-of-range error with the lower bound that failed.
327    #[must_use]
328    pub const fn with_lower_bound(self, lower_bound: IntegerValue) -> Self {
329        Self {
330            actual: self.actual,
331            lower_bound: Some(lower_bound),
332            upper_bound: self.upper_bound,
333        }
334    }
335
336    /// Returns an out-of-range error with the upper bound that failed.
337    #[must_use]
338    pub const fn with_upper_bound(self, upper_bound: IntegerValue) -> Self {
339        Self {
340            actual: self.actual,
341            lower_bound: self.lower_bound,
342            upper_bound: Some(upper_bound),
343        }
344    }
345
346    /// Returns the actual value that failed validation.
347    pub const fn actual(&self) -> IntegerValue {
348        self.actual
349    }
350
351    /// Returns the lower bound that failed when it could be captured losslessly.
352    pub const fn lower_bound(&self) -> Option<IntegerValue> {
353        self.lower_bound
354    }
355
356    /// Returns the upper bound that failed when it could be captured losslessly.
357    pub const fn upper_bound(&self) -> Option<IntegerValue> {
358        self.upper_bound
359    }
360}
361
362impl fmt::Display for OutOfRangeIntegerError {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        let actual = self.actual;
365        match (self.lower_bound, self.upper_bound) {
366            (Some(lower_bound), Some(upper_bound)) => {
367                write!(
368                    f,
369                    "out of range (value {actual}, lower bound {lower_bound}, upper bound {upper_bound})"
370                )
371            }
372            (Some(lower_bound), None) => {
373                write!(
374                    f,
375                    "out of range (value {actual}, lower bound {lower_bound})"
376                )
377            }
378            (None, Some(upper_bound)) => {
379                write!(
380                    f,
381                    "out of range (value {actual}, upper bound {upper_bound})"
382                )
383            }
384            (None, None) => write!(f, "out of range (value {actual})"),
385        }
386    }
387}
388
389impl StdError for OutOfRangeIntegerError {}
390
391/// Error returned when a float newtype is outside its `range` bounds.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct OutOfRangeFloatError {
394    actual: FloatValue,
395    lower_bound: Option<FloatValue>,
396    upper_bound: Option<FloatValue>,
397    violation: FloatRangeViolation,
398}
399
400impl OutOfRangeFloatError {
401    /// Creates an error for a value that cannot be compared to range bounds.
402    pub const fn not_comparable(actual: FloatValue) -> Self {
403        Self {
404            actual,
405            lower_bound: None,
406            upper_bound: None,
407            violation: FloatRangeViolation::NotComparable,
408        }
409    }
410
411    /// Creates an error for a value below the lower bound.
412    pub const fn below_lower_bound(actual: FloatValue, lower_bound: FloatValue) -> Self {
413        Self {
414            actual,
415            lower_bound: Some(lower_bound),
416            upper_bound: None,
417            violation: FloatRangeViolation::BelowLowerBound,
418        }
419    }
420
421    /// Creates an error for a value above the upper bound.
422    pub const fn above_upper_bound(actual: FloatValue, upper_bound: FloatValue) -> Self {
423        Self {
424            actual,
425            lower_bound: None,
426            upper_bound: Some(upper_bound),
427            violation: FloatRangeViolation::AboveUpperBound,
428        }
429    }
430
431    /// Returns the actual value that failed validation.
432    pub const fn actual(&self) -> FloatValue {
433        self.actual
434    }
435
436    /// Returns the lower bound that failed.
437    pub const fn lower_bound(&self) -> Option<FloatValue> {
438        self.lower_bound
439    }
440
441    /// Returns the upper bound that failed.
442    pub const fn upper_bound(&self) -> Option<FloatValue> {
443        self.upper_bound
444    }
445
446    /// Returns why range validation failed.
447    pub const fn violation(&self) -> FloatRangeViolation {
448        self.violation
449    }
450}
451
452impl fmt::Display for OutOfRangeFloatError {
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        let actual = self.actual;
455        match self.violation {
456            FloatRangeViolation::NotComparable => write!(f, "not comparable (value {actual})"),
457            FloatRangeViolation::BelowLowerBound => match self.lower_bound {
458                Some(lower_bound) => {
459                    write!(
460                        f,
461                        "out of range (value {actual}, lower bound {lower_bound})"
462                    )
463                }
464                None => write!(f, "out of range (value {actual})"),
465            },
466            FloatRangeViolation::AboveUpperBound => match self.upper_bound {
467                Some(upper_bound) => {
468                    write!(
469                        f,
470                        "out of range (value {actual}, upper bound {upper_bound})"
471                    )
472                }
473                None => write!(f, "out of range (value {actual})"),
474            },
475        }
476    }
477}
478
479impl StdError for OutOfRangeFloatError {}
480
481#[cfg(feature = "valuable")]
482static OUT_OF_RANGE_INTEGER_ERROR_FIELDS: &[NamedField<'static>] = &[
483    NamedField::new("actual"),
484    NamedField::new("lower_bound"),
485    NamedField::new("upper_bound"),
486];
487
488#[cfg(feature = "valuable")]
489impl Valuable for OutOfRangeIntegerError {
490    fn as_value(&self) -> Value<'_> {
491        Value::Structable(self)
492    }
493
494    fn visit(&self, visit: &mut dyn Visit) {
495        let values = [
496            self.actual.as_value(),
497            self.lower_bound.as_value(),
498            self.upper_bound.as_value(),
499        ];
500        visit.visit_named_fields(&NamedValues::new(
501            OUT_OF_RANGE_INTEGER_ERROR_FIELDS,
502            &values,
503        ));
504    }
505}
506
507#[cfg(feature = "valuable")]
508impl Structable for OutOfRangeIntegerError {
509    fn definition(&self) -> StructDef<'_> {
510        StructDef::new_static(
511            "OutOfRangeIntegerError",
512            Fields::Named(OUT_OF_RANGE_INTEGER_ERROR_FIELDS),
513        )
514    }
515}
516
517#[cfg(feature = "valuable")]
518static OUT_OF_RANGE_FLOAT_ERROR_FIELDS: &[NamedField<'static>] = &[
519    NamedField::new("actual"),
520    NamedField::new("lower_bound"),
521    NamedField::new("upper_bound"),
522    NamedField::new("violation"),
523];
524
525#[cfg(feature = "valuable")]
526impl Valuable for OutOfRangeFloatError {
527    fn as_value(&self) -> Value<'_> {
528        Value::Structable(self)
529    }
530
531    fn visit(&self, visit: &mut dyn Visit) {
532        let values = [
533            self.actual.as_value(),
534            self.lower_bound.as_value(),
535            self.upper_bound.as_value(),
536            self.violation.as_value(),
537        ];
538        visit.visit_named_fields(&NamedValues::new(OUT_OF_RANGE_FLOAT_ERROR_FIELDS, &values));
539    }
540}
541
542#[cfg(feature = "valuable")]
543impl Structable for OutOfRangeFloatError {
544    fn definition(&self) -> StructDef<'_> {
545        StructDef::new_static(
546            "OutOfRangeFloatError",
547            Fields::Named(OUT_OF_RANGE_FLOAT_ERROR_FIELDS),
548        )
549    }
550}
551
552/// Error returned when a string newtype contains a character rejected by `chars`.
553#[derive(Debug, Clone, PartialEq, Eq)]
554pub struct InvalidCharError {
555    index: usize,
556    ch: char,
557}
558
559impl InvalidCharError {
560    /// Creates an invalid-character error.
561    pub const fn new(index: usize, ch: char) -> Self {
562        Self { index, ch }
563    }
564
565    /// Returns the zero-based character index, not a byte offset.
566    pub const fn index(&self) -> usize {
567        self.index
568    }
569
570    /// Returns the rejected character.
571    pub const fn ch(&self) -> char {
572        self.ch
573    }
574}
575
576impl fmt::Display for InvalidCharError {
577    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578        write!(
579            f,
580            "contains invalid character '{}' at index {}",
581            self.ch, self.index
582        )
583    }
584}
585
586impl StdError for InvalidCharError {}
587
588/// Common interface for generated Vouched errors.
589///
590/// Error types generated by the Vouched derive macro implement this trait.
591/// The `as_*` methods provide type-safe access to specific validation errors.
592pub trait VouchedError: StdError + Send + Sync + 'static {
593    /// Returns the underlying too-short error when this is that variant.
594    fn as_too_short(&self) -> Option<&TooShortError> {
595        None
596    }
597    /// Returns the underlying too-long error when this is that variant.
598    fn as_too_long(&self) -> Option<&TooLongError> {
599        None
600    }
601    /// Returns the underlying integer out-of-range error when this is that variant.
602    fn as_out_of_range_integer(&self) -> Option<&OutOfRangeIntegerError> {
603        None
604    }
605    /// Returns the underlying float out-of-range error when this is that variant.
606    fn as_out_of_range_float(&self) -> Option<&OutOfRangeFloatError> {
607        None
608    }
609    /// Returns the underlying invalid-character error when this is that variant.
610    fn as_invalid_char(&self) -> Option<&InvalidCharError> {
611        None
612    }
613}
614
615/// Wrapper type for handling different Vouched error types uniformly.
616///
617/// This allows code that works with multiple Vouched wrappers to handle their
618/// validation errors through a single type. It supports automatic conversion
619/// through the `?` operator.
620///
621/// Available with the `alloc` feature, which is enabled by the default `std`
622/// feature.
623///
624/// # Examples
625///
626/// ```
627/// # use vouched_core::*;
628/// #
629/// # #[derive(Debug)]
630/// # enum UserIdError { TooShort(TooShortError), TooLong(TooLongError) }
631/// # impl core::fmt::Display for UserIdError {
632/// #     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
633/// #         match self { UserIdError::TooShort(e) => e.fmt(f), UserIdError::TooLong(e) => e.fmt(f) }
634/// #     }
635/// # }
636/// # impl core::error::Error for UserIdError {}
637/// # impl VouchedError for UserIdError {
638/// #     fn as_too_short(&self) -> Option<&TooShortError> {
639/// #         match self { UserIdError::TooShort(e) => Some(e), _ => None }
640/// #     }
641/// #     fn as_too_long(&self) -> Option<&TooLongError> {
642/// #         match self { UserIdError::TooLong(e) => Some(e), _ => None }
643/// #     }
644/// # }
645/// #
646/// // Function that handles different Vouched error types uniformly.
647/// fn process_errors() -> Result<(), Error> {
648///     // Any error implementing VouchedError can be converted into Error.
649///     let err = UserIdError::TooShort(TooShortError::new(1, 0));
650///     Err(Error::from(err))
651/// }
652///
653/// // Inspect the concrete validation error kind.
654/// let result = process_errors();
655/// assert!(result.is_err());
656/// let err = result.unwrap_err();
657/// assert!(err.as_too_short().is_some());
658/// ```
659#[cfg(feature = "alloc")]
660#[derive(Debug)]
661pub struct Error(Box<dyn VouchedError>);
662
663#[cfg(feature = "alloc")]
664impl Error {
665    /// Wraps a boxed generated Vouched error.
666    pub fn new(inner: Box<dyn VouchedError>) -> Self {
667        Self(inner)
668    }
669
670    /// Returns the wrapped generated Vouched error.
671    pub fn into_inner(self) -> Box<dyn VouchedError> {
672        self.0
673    }
674}
675
676#[cfg(feature = "alloc")]
677impl fmt::Display for Error {
678    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679        self.0.fmt(f)
680    }
681}
682
683#[cfg(feature = "alloc")]
684impl StdError for Error {
685    fn source(&self) -> Option<&(dyn StdError + 'static)> {
686        Some(&*self.0)
687    }
688}
689
690#[cfg(feature = "alloc")]
691impl<E> From<E> for Error
692where
693    E: VouchedError,
694{
695    fn from(e: E) -> Self {
696        Self(Box::new(e))
697    }
698}
699
700#[cfg(feature = "alloc")]
701impl Deref for Error {
702    type Target = dyn VouchedError;
703
704    fn deref(&self) -> &Self::Target {
705        &*self.0
706    }
707}
708
709#[cfg(feature = "alloc")]
710impl AsRef<dyn VouchedError> for Error {
711    fn as_ref(&self) -> &(dyn VouchedError + 'static) {
712        &*self.0
713    }
714}
715
716#[cfg(feature = "valuable")]
717static ERROR_FIELDS: &[NamedField<'static>] = &[NamedField::new("message")];
718
719#[cfg(feature = "valuable")]
720impl Valuable for Error {
721    fn as_value(&self) -> Value<'_> {
722        Value::Structable(self)
723    }
724
725    fn visit(&self, visit: &mut dyn Visit) {
726        let message = self.to_string();
727        let values = [message.as_value()];
728        visit.visit_named_fields(&NamedValues::new(ERROR_FIELDS, &values));
729    }
730}
731
732#[cfg(feature = "valuable")]
733impl Structable for Error {
734    fn definition(&self) -> StructDef<'_> {
735        StructDef::new_static("Error", Fields::Named(ERROR_FIELDS))
736    }
737}