Skip to main content

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, Eq, Hash)]
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    #[must_use]
163    pub fn parse_error(message: impl Into<String>, input: impl Into<String>) -> Self {
164        Self::ParseError {
165            message: message.into(),
166            input: input.into(),
167            position: None,
168        }
169    }
170
171    /// Creates a new parse error with position information.
172    ///
173    /// Use this when you know exactly where in the input the parse error occurred.
174    ///
175    /// # Arguments
176    ///
177    /// * `message` - Description of what went wrong
178    /// * `input` - The input string that failed to parse
179    /// * `position` - Character position where parsing failed
180    ///
181    /// # Example
182    ///
183    /// ```
184    /// use temps_core::TempsError;
185    ///
186    /// let err = TempsError::parse_error_with_position(
187    ///     "Unexpected character",
188    ///     "in 5 minuts",
189    ///     9  // Points to the 't' in "minuts"
190    /// );
191    /// ```
192    #[must_use]
193    pub fn parse_error_with_position(
194        message: impl Into<String>,
195        input: impl Into<String>,
196        position: usize,
197    ) -> Self {
198        Self::ParseError {
199            message: message.into(),
200            input: input.into(),
201            position: Some(position),
202        }
203    }
204
205    /// Creates a new date calculation error.
206    ///
207    /// Use this for errors that occur during date arithmetic operations.
208    ///
209    /// # Example
210    ///
211    /// ```
212    /// use temps_core::TempsError;
213    ///
214    /// let err = TempsError::date_calculation(
215    ///     "Cannot subtract 13 months from January"
216    /// );
217    /// ```
218    #[must_use]
219    pub fn date_calculation(message: impl Into<String>) -> Self {
220        Self::DateCalculationError {
221            message: message.into(),
222            context: None,
223        }
224    }
225
226    /// Creates a new date calculation error with additional context.
227    ///
228    /// Use this when you want to include information about what caused
229    /// the calculation to fail (e.g., an error from the backend library).
230    ///
231    /// # Example
232    ///
233    /// ```
234    /// use temps_core::TempsError;
235    ///
236    /// let err = TempsError::date_calculation_with_source(
237    ///     "Failed to add months",
238    ///     "chronos error: date out of range"
239    /// );
240    /// ```
241    #[must_use]
242    pub fn date_calculation_with_source(
243        message: impl Into<String>,
244        context: impl Into<String>,
245    ) -> Self {
246        Self::DateCalculationError {
247            message: message.into(),
248            context: Some(context.into()),
249        }
250    }
251
252    /// Creates an invalid date error
253    #[must_use]
254    pub fn invalid_date(year: u16, month: u8, day: u8) -> Self {
255        Self::InvalidDate { year, month, day }
256    }
257
258    /// Creates an invalid time error
259    #[must_use]
260    pub fn invalid_time(hour: u8, minute: u8, second: u8) -> Self {
261        Self::InvalidTime {
262            hour,
263            minute,
264            second,
265        }
266    }
267
268    /// Creates an invalid timezone offset error
269    #[must_use]
270    pub fn invalid_timezone_offset(hours: i8, minutes: u8) -> Self {
271        Self::InvalidTimezoneOffset { hours, minutes }
272    }
273
274    /// Creates an ambiguous time error
275    #[must_use]
276    pub fn ambiguous_time(message: impl Into<String>) -> Self {
277        Self::AmbiguousTime {
278            message: message.into(),
279        }
280    }
281
282    /// Creates an arithmetic overflow error
283    #[must_use]
284    pub fn arithmetic_overflow(operation: impl Into<String>) -> Self {
285        Self::ArithmeticOverflow {
286            operation: operation.into(),
287        }
288    }
289
290    /// Creates an unsupported operation error
291    #[must_use]
292    pub fn unsupported_operation(operation: impl Into<String>) -> Self {
293        Self::UnsupportedOperation {
294            operation: operation.into(),
295        }
296    }
297
298    /// Creates a backend error
299    #[must_use]
300    pub fn backend_error(message: impl Into<String>, backend: impl Into<String>) -> Self {
301        Self::BackendError {
302            message: message.into(),
303            backend: backend.into(),
304        }
305    }
306}
307
308/// Result type alias for temps operations.
309///
310/// All parsing and time calculation operations in the temps library
311/// return this result type.
312///
313/// # Example
314///
315/// ```
316/// use temps_core::Result;
317///
318/// fn parse_time(input: &str) -> Result<String> {
319///     // Implementation
320///     Ok("parsed".to_string())
321/// }
322/// ```
323pub type Result<T> = std::result::Result<T, TempsError>;
324
325/// Extension trait for converting parser errors to TempsError.
326///
327/// This trait is implemented for winnow parser errors to provide
328/// convenient conversion to our error type.
329pub trait ParseErrorExt {
330    /// Convert a parser error to a TempsError.
331    ///
332    /// This method extracts position information from the parser error
333    /// and creates a properly formatted TempsError.
334    fn to_temps_error(self, input: &str) -> TempsError;
335}
336
337impl ParseErrorExt for winnow::error::ParseError<&str, winnow::error::ContextError> {
338    fn to_temps_error(self, input: &str) -> TempsError {
339        let position = self.offset();
340        let message = format!("Parser error: {self}");
341        TempsError::parse_error_with_position(message, input, position)
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_error_display() {
351        let err = TempsError::invalid_date(2024, 13, 32);
352        assert_eq!(err.to_string(), "Invalid date: year=2024, month=13, day=32");
353
354        let err = TempsError::invalid_time(25, 61, 61);
355        assert_eq!(err.to_string(), "Invalid time: 25:61:61");
356
357        let err = TempsError::parse_error("unexpected token", "in 5 minuts");
358        assert_eq!(
359            err.to_string(),
360            "Failed to parse time expression: unexpected token"
361        );
362    }
363
364    #[test]
365    fn test_error_creation_helpers() {
366        let err = TempsError::date_calculation("month out of range");
367        match err {
368            TempsError::DateCalculationError { message, context } => {
369                assert_eq!(message, "month out of range");
370                assert!(context.is_none());
371            }
372            _ => panic!("Wrong error type"),
373        }
374
375        let err = TempsError::backend_error("conversion failed", "chrono");
376        match err {
377            TempsError::BackendError { message, backend } => {
378                assert_eq!(message, "conversion failed");
379                assert_eq!(backend, "chrono");
380            }
381            _ => panic!("Wrong error type"),
382        }
383    }
384}