Skip to main content

regit_daycount/
errors.rs

1// Copyright 2026 Regit.io — Nicolas Koenig
2// SPDX-License-Identifier: Apache-2.0
3
4//! Typed error enum for date construction and convention dispatch.
5//!
6//! All failure paths return a typed `Result` — no `panic!()`, no `unwrap()`,
7//! no string errors. Each variant carries enough context for the caller to
8//! report precisely what was wrong and where.
9//!
10//! A single enum covers the two failure domains the crate can produce:
11//!
12//! - [`ValidationError::InvalidDate`] — a `(year, month, day)` triple does
13//!   not name a real Gregorian date (e.g. 31 February, a 0 month).
14//! - [`ValidationError::OutOfRange`] — a value is well-formed in isolation
15//!   but falls outside the range a convention or calendar accepts.
16//!
17//! It implements [`core::fmt::Display`] and [`core::error::Error`], so it
18//! composes with `?` and with `dyn Error` even under `#![no_std]`.
19//!
20//! # References
21//!
22//! - ISO 8601 (Gregorian calendar date format) — the date grammar these
23//!   errors report against.
24
25use core::fmt;
26
27// ─── Validation errors ───────────────────────────────────────────────────────
28
29/// Error returned when an input cannot be accepted as a valid date or when a
30/// value falls outside the range a convention accepts.
31///
32/// The crate has a single error type because every failure mode reduces to
33/// one of two shapes: an input that does not name a real date, or an input
34/// that is well-formed in isolation but outside the supported range.
35///
36/// # Examples
37///
38/// ```
39/// use regit_daycount::errors::ValidationError;
40///
41/// let err = ValidationError::InvalidDate { rule: "day-out-of-range" };
42/// assert_eq!(err, ValidationError::InvalidDate { rule: "day-out-of-range" });
43/// ```
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ValidationError {
46    /// The `(year, month, day)` triple does not name a real Gregorian date;
47    /// `rule` names the violated structural rule.
48    InvalidDate {
49        /// A short, human-readable description of the violated rule.
50        rule: &'static str,
51    },
52    /// A value is well-formed in isolation but falls outside the range the
53    /// caller's convention or calendar accepts; `what` names the value.
54    OutOfRange {
55        /// A short, human-readable description of the out-of-range value.
56        what: &'static str,
57    },
58}
59
60impl fmt::Display for ValidationError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Self::InvalidDate { rule } => write!(f, "invalid date: {rule}"),
64            Self::OutOfRange { what } => write!(f, "value out of range: {what}"),
65        }
66    }
67}
68
69impl core::error::Error for ValidationError {}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::test_support::{debug, display};
75
76    #[test]
77    fn validation_error_display_invalid_date() {
78        let err = ValidationError::InvalidDate {
79            rule: "month-out-of-range",
80        };
81        assert_eq!(display(err).as_str(), "invalid date: month-out-of-range");
82    }
83
84    #[test]
85    fn validation_error_display_out_of_range() {
86        let err = ValidationError::OutOfRange {
87            what: "year < 1583",
88        };
89        assert_eq!(display(err).as_str(), "value out of range: year < 1583");
90    }
91
92    #[test]
93    fn validation_error_display_has_no_trailing_period() {
94        for err in [
95            ValidationError::InvalidDate { rule: "r" },
96            ValidationError::OutOfRange { what: "w" },
97        ] {
98            assert!(!display(err).as_str().ends_with('.'));
99        }
100    }
101
102    #[test]
103    fn validation_error_is_error_trait() {
104        let err: &dyn core::error::Error = &ValidationError::InvalidDate { rule: "r" };
105        assert!(err.source().is_none());
106    }
107
108    #[test]
109    fn validation_error_copy_eq() {
110        let err = ValidationError::OutOfRange { what: "year" };
111        let copy = err;
112        assert_eq!(err, copy);
113    }
114
115    #[test]
116    fn errors_debug() {
117        assert!(
118            debug(ValidationError::InvalidDate { rule: "r" })
119                .as_str()
120                .contains("InvalidDate")
121        );
122        assert!(
123            debug(ValidationError::OutOfRange { what: "w" })
124                .as_str()
125                .contains("OutOfRange")
126        );
127    }
128}