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}