temps_core/
error.rs

1//! Error types for the temps library.
2//!
3//! This module defines the error types used throughout the temps ecosystem.
4//! All parsing and date calculation operations return `Result<T, TempsError>`.
5//!
6//! # Error Categories
7//!
8//! - **Parse Errors**: When input cannot be parsed as a valid time expression
9//! - **Date Calculation Errors**: When date arithmetic results in invalid dates
10//! - **Invalid Component Errors**: When date/time components are out of range
11//! - **Backend Errors**: When the underlying datetime library reports an error
12//!
13//! # Examples
14//!
15//! ```
16//! use temps_core::{parse, Language, TempsError};
17//!
18//! // Parse error example
19//! let result = parse("invalid input", Language::English);
20//! match result {
21//!     Err(TempsError::ParseError { message, input, position }) => {
22//!         println!("Parse failed: {}", message);
23//!     }
24//!     _ => {}
25//! }
26//! ```
27
28use thiserror::Error;
29
30/// The main error type for the temps library.
31///
32/// This enum represents all possible errors that can occur during
33/// parsing and time calculation operations.
34#[derive(Error, Debug, Clone, PartialEq)]
35pub enum TempsError {
36    /// Error that occurs during parsing of time expressions.
37    ///
38    /// This error is returned when the input string cannot be parsed
39    /// as a valid time expression in the specified language.
40    ///
41    /// # Example
42    ///
43    /// ```
44    /// use temps_core::TempsError;
45    ///
46    /// let err = TempsError::parse_error("Unrecognized time unit", "in 5 blargs");
47    /// ```
48    #[error("Failed to parse time expression: {message}")]
49    ParseError {
50        /// The specific parsing error message
51        message: String,
52        /// The input that failed to parse
53        input: String,
54        /// Optional position in the input where parsing failed
55        position: Option<usize>,
56    },
57
58    /// Error that occurs during date/time calculations.
59    ///
60    /// This error is returned when date arithmetic operations fail,
61    /// such as when adding months to January 31st would result in
62    /// February 31st (which doesn't exist).
63    ///
64    /// # Example
65    ///
66    /// ```
67    /// use temps_core::TempsError;
68    ///
69    /// let err = TempsError::date_calculation("Month overflow");
70    /// ```
71    #[error("Date calculation error: {message}")]
72    DateCalculationError {
73        /// The specific calculation error message
74        message: String,
75        /// Optional context about what caused the error
76        context: Option<String>,
77    },
78
79    /// Error for invalid date components
80    #[error("Invalid date: year={year}, month={month}, day={day}")]
81    InvalidDate {
82        /// The year component
83        year: u16,
84        /// The month component (1-12)
85        month: u8,
86        /// The day component (1-31)
87        day: u8,
88    },
89
90    /// Error for invalid time components
91    #[error("Invalid time: {hour:02}:{minute:02}:{second:02}")]
92    InvalidTime {
93        /// The hour component (0-23)
94        hour: u8,
95        /// The minute component (0-59)
96        minute: u8,
97        /// The second component (0-59)
98        second: u8,
99    },
100
101    /// Error for invalid timezone offset
102    #[error("Invalid timezone offset: {hours:+03}:{minutes:02}")]
103    InvalidTimezoneOffset {
104        /// The hour offset (-12 to +14)
105        hours: i8,
106        /// The minute offset (0-59)
107        minutes: u8,
108    },
109
110    /// Error for ambiguous local time (e.g., during DST transitions)
111    #[error("Ambiguous local time: {message}")]
112    AmbiguousTime {
113        /// Description of the ambiguity
114        message: String,
115    },
116
117    /// Error for arithmetic overflow in date calculations
118    #[error("Arithmetic overflow: {operation}")]
119    ArithmeticOverflow {
120        /// The operation that caused the overflow
121        operation: String,
122    },
123
124    /// Error for unsupported operations
125    #[error("Unsupported operation: {operation}")]
126    UnsupportedOperation {
127        /// Description of the unsupported operation
128        operation: String,
129    },
130
131    /// Error from the underlying datetime backend (chrono, jiff, etc.)
132    #[error("Backend error: {message}")]
133    BackendError {
134        /// The error message from the backend
135        message: String,
136        /// The backend that produced the error
137        backend: String,
138    },
139}
140
141impl TempsError {
142    /// Creates a new parse error without position information.
143    ///
144    /// Use this when you know parsing failed but don't have a specific
145    /// position in the input where the error occurred.
146    ///
147    /// # Arguments
148    ///
149    /// * `message` - Description of what went wrong
150    /// * `input` - The input string that failed to parse
151    ///
152    /// # Example
153    ///
154    /// ```
155    /// use temps_core::TempsError;
156    ///
157    /// let err = TempsError::parse_error(
158    ///     "Expected time unit",
159    ///     "in 5"
160    /// );
161    /// ```
162    pub fn parse_error(message: impl Into<String>, input: impl Into<String>) -> Self {
163        Self::ParseError {
164            message: message.into(),
165            input: input.into(),
166            position: None,
167        }
168    }
169
170    /// Creates a new parse error with position information.
171    ///
172    /// Use this when you know exactly where in the input the parse error occurred.
173    ///
174    /// # Arguments
175    ///
176    /// * `message` - Description of what went wrong
177    /// * `input` - The input string that failed to parse
178    /// * `position` - Character position where parsing failed
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use temps_core::TempsError;
184    ///
185    /// let err = TempsError::parse_error_with_position(
186    ///     "Unexpected character",
187    ///     "in 5 minuts",
188    ///     9  // Points to the 't' in "minuts"
189    /// );
190    /// ```
191    pub fn parse_error_with_position(
192        message: impl Into<String>,
193        input: impl Into<String>,
194        position: usize,
195    ) -> Self {
196        Self::ParseError {
197            message: message.into(),
198            input: input.into(),
199            position: Some(position),
200        }
201    }
202
203    /// Creates a new date calculation error.
204    ///
205    /// Use this for errors that occur during date arithmetic operations.
206    ///
207    /// # Example
208    ///
209    /// ```
210    /// use temps_core::TempsError;
211    ///
212    /// let err = TempsError::date_calculation(
213    ///     "Cannot subtract 13 months from January"
214    /// );
215    /// ```
216    pub fn date_calculation(message: impl Into<String>) -> Self {
217        Self::DateCalculationError {
218            message: message.into(),
219            context: None,
220        }
221    }
222
223    /// Creates a new date calculation error with additional context.
224    ///
225    /// Use this when you want to include information about what caused
226    /// the calculation to fail (e.g., an error from the backend library).
227    ///
228    /// # Example
229    ///
230    /// ```
231    /// use temps_core::TempsError;
232    ///
233    /// let err = TempsError::date_calculation_with_source(
234    ///     "Failed to add months",
235    ///     "chronos error: date out of range"
236    /// );
237    /// ```
238    pub fn date_calculation_with_source(
239        message: impl Into<String>,
240        context: impl Into<String>,
241    ) -> Self {
242        Self::DateCalculationError {
243            message: message.into(),
244            context: Some(context.into()),
245        }
246    }
247
248    /// Creates an invalid date error
249    pub fn invalid_date(year: u16, month: u8, day: u8) -> Self {
250        Self::InvalidDate { year, month, day }
251    }
252
253    /// Creates an invalid time error
254    pub fn invalid_time(hour: u8, minute: u8, second: u8) -> Self {
255        Self::InvalidTime {
256            hour,
257            minute,
258            second,
259        }
260    }
261
262    /// Creates an invalid timezone offset error
263    pub fn invalid_timezone_offset(hours: i8, minutes: u8) -> Self {
264        Self::InvalidTimezoneOffset { hours, minutes }
265    }
266
267    /// Creates an ambiguous time error
268    pub fn ambiguous_time(message: impl Into<String>) -> Self {
269        Self::AmbiguousTime {
270            message: message.into(),
271        }
272    }
273
274    /// Creates an arithmetic overflow error
275    pub fn arithmetic_overflow(operation: impl Into<String>) -> Self {
276        Self::ArithmeticOverflow {
277            operation: operation.into(),
278        }
279    }
280
281    /// Creates an unsupported operation error
282    pub fn unsupported_operation(operation: impl Into<String>) -> Self {
283        Self::UnsupportedOperation {
284            operation: operation.into(),
285        }
286    }
287
288    /// Creates a backend error
289    pub fn backend_error(message: impl Into<String>, backend: impl Into<String>) -> Self {
290        Self::BackendError {
291            message: message.into(),
292            backend: backend.into(),
293        }
294    }
295}
296
297/// Result type alias for temps operations.
298///
299/// All parsing and time calculation operations in the temps library
300/// return this result type.
301///
302/// # Example
303///
304/// ```
305/// use temps_core::Result;
306///
307/// fn parse_time(input: &str) -> Result<String> {
308///     // Implementation
309///     Ok("parsed".to_string())
310/// }
311/// ```
312pub type Result<T> = std::result::Result<T, TempsError>;
313
314/// Extension trait for converting parser errors to TempsError.
315///
316/// This trait is implemented for winnow parser errors to provide
317/// convenient conversion to our error type.
318pub trait ParseErrorExt {
319    /// Convert a parser error to a TempsError.
320    ///
321    /// This method extracts position information from the parser error
322    /// and creates a properly formatted TempsError.
323    fn to_temps_error(self, input: &str) -> TempsError;
324}
325
326impl ParseErrorExt for winnow::error::ParseError<&str, winnow::error::ContextError> {
327    fn to_temps_error(self, input: &str) -> TempsError {
328        let position = self.offset();
329        let message = format!("Parser error: {self}");
330        TempsError::parse_error_with_position(message, input, position)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_error_display() {
340        let err = TempsError::invalid_date(2024, 13, 32);
341        assert_eq!(err.to_string(), "Invalid date: year=2024, month=13, day=32");
342
343        let err = TempsError::invalid_time(25, 61, 61);
344        assert_eq!(err.to_string(), "Invalid time: 25:61:61");
345
346        let err = TempsError::parse_error("unexpected token", "in 5 minuts");
347        assert_eq!(
348            err.to_string(),
349            "Failed to parse time expression: unexpected token"
350        );
351    }
352
353    #[test]
354    fn test_error_creation_helpers() {
355        let err = TempsError::date_calculation("month out of range");
356        match err {
357            TempsError::DateCalculationError { message, context } => {
358                assert_eq!(message, "month out of range");
359                assert!(context.is_none());
360            }
361            _ => panic!("Wrong error type"),
362        }
363
364        let err = TempsError::backend_error("conversion failed", "chrono");
365        match err {
366            TempsError::BackendError { message, backend } => {
367                assert_eq!(message, "conversion failed");
368                assert_eq!(backend, "chrono");
369            }
370            _ => panic!("Wrong error type"),
371        }
372    }
373}