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}