finance_query/
error.rs

1use thiserror::Error;
2
3/// Main error type for the library
4#[derive(Error, Debug)]
5pub enum YahooError {
6    /// Authentication with Yahoo Finance failed
7    #[error("Authentication failed: {context}")]
8    AuthenticationFailed {
9        /// Error context
10        context: String,
11    },
12
13    /// The requested symbol was not found
14    #[error("Symbol not found: {}", symbol.as_ref().map(|s| s.as_str()).unwrap_or("unknown"))]
15    SymbolNotFound {
16        /// The symbol that was not found
17        symbol: Option<String>,
18        /// Additional context
19        context: String,
20    },
21
22    /// Rate limit exceeded
23    #[error("Rate limited (retry after {retry_after:?}s)")]
24    RateLimited {
25        /// Seconds until retry is allowed
26        retry_after: Option<u64>,
27    },
28
29    /// HTTP request error
30    #[error("HTTP request failed: {0}")]
31    HttpError(#[from] reqwest::Error),
32
33    /// Failed to parse JSON response
34    #[error("JSON parse error: {0}")]
35    JsonParseError(#[from] serde_json::Error),
36
37    /// Response structure error - missing or malformed fields
38    #[error("Response structure error in '{field}': {context}")]
39    ResponseStructureError {
40        /// Field name that caused the error
41        field: String,
42        /// Error context
43        context: String,
44    },
45
46    /// Invalid parameter provided
47    #[error("Invalid parameter '{param}': {reason}")]
48    InvalidParameter {
49        /// Parameter name
50        param: String,
51        /// Reason for invalidity
52        reason: String,
53    },
54
55    /// Network timeout
56    #[error("Request timeout after {timeout_ms}ms")]
57    Timeout {
58        /// Timeout duration in milliseconds
59        timeout_ms: u64,
60    },
61
62    /// Server error (5xx status codes)
63    #[error("Server error {status}: {context}")]
64    ServerError {
65        /// HTTP status code
66        status: u16,
67        /// Error context
68        context: String,
69    },
70
71    /// Unexpected response from Yahoo Finance
72    #[error("Unexpected response: {0}")]
73    UnexpectedResponse(String),
74
75    /// Internal error
76    #[error("Internal error: {0}")]
77    InternalError(String),
78
79    /// API error from Yahoo Finance
80    #[error("API error: {0}")]
81    ApiError(String),
82
83    /// Tokio runtime error
84    #[error("Runtime error: {0}")]
85    RuntimeError(#[from] std::io::Error),
86}
87
88/// Error category for logging and metrics
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum ErrorCategory {
91    /// Authentication errors
92    Auth,
93    /// Rate limiting errors
94    RateLimit,
95    /// Timeout errors
96    Timeout,
97    /// Server errors (5xx)
98    Server,
99    /// Not found errors
100    NotFound,
101    /// Validation errors
102    Validation,
103    /// Parsing errors
104    Parsing,
105    /// Other errors
106    Other,
107}
108
109/// Type alias for Error (for consistency with common Rust patterns)
110pub type Error = YahooError;
111
112/// Result type alias for library operations
113pub type Result<T> = std::result::Result<T, YahooError>;
114
115impl YahooError {
116    /// Check if this error is retriable
117    pub fn is_retriable(&self) -> bool {
118        matches!(
119            self,
120            YahooError::Timeout { .. }
121                | YahooError::RateLimited { .. }
122                | YahooError::HttpError(_)
123                | YahooError::AuthenticationFailed { .. }
124                | YahooError::ServerError { .. }
125        )
126    }
127
128    /// Check if this error indicates an authentication issue
129    pub fn is_auth_error(&self) -> bool {
130        matches!(self, YahooError::AuthenticationFailed { .. })
131    }
132
133    /// Check if this error indicates a not found issue
134    pub fn is_not_found(&self) -> bool {
135        matches!(self, YahooError::SymbolNotFound { .. })
136    }
137
138    /// Get retry delay in seconds (for exponential backoff)
139    pub fn retry_after_secs(&self) -> Option<u64> {
140        match self {
141            Self::RateLimited { retry_after } => *retry_after,
142            Self::Timeout { .. } => Some(2),
143            Self::ServerError { status, .. } if *status >= 500 => Some(5),
144            Self::AuthenticationFailed { .. } => Some(1),
145            _ => None,
146        }
147    }
148
149    /// Categorize errors for logging/metrics
150    pub fn category(&self) -> ErrorCategory {
151        match self {
152            Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
153            Self::RateLimited { .. } => ErrorCategory::RateLimit,
154            Self::Timeout { .. } => ErrorCategory::Timeout,
155            Self::ServerError { .. } => ErrorCategory::Server,
156            Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
157            Self::InvalidParameter { .. } => ErrorCategory::Validation,
158            Self::JsonParseError(_) | Self::ResponseStructureError { .. } => ErrorCategory::Parsing,
159            _ => ErrorCategory::Other,
160        }
161    }
162
163    /// Add symbol context to error (fluent API)
164    pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
165        if let Self::SymbolNotFound {
166            symbol: ref mut s, ..
167        } = self
168        {
169            *s = Some(symbol.into());
170        }
171        self
172    }
173
174    /// Add context to error (fluent API)
175    pub fn with_context(mut self, context: impl Into<String>) -> Self {
176        match self {
177            Self::AuthenticationFailed {
178                context: ref mut c, ..
179            } => {
180                *c = context.into();
181            }
182            Self::SymbolNotFound {
183                context: ref mut c, ..
184            } => {
185                *c = context.into();
186            }
187            Self::ResponseStructureError {
188                context: ref mut c, ..
189            } => {
190                *c = context.into();
191            }
192            Self::ServerError {
193                context: ref mut c, ..
194            } => {
195                *c = context.into();
196            }
197            _ => {}
198        }
199        self
200    }
201}
202
203// Backward compatibility: Allow ParseError to be created from String
204impl YahooError {
205    /// Create a ParseError from a string (for backward compatibility)
206    #[deprecated(since = "2.0.0", note = "Use ResponseStructureError instead")]
207    pub fn parse_error(msg: impl Into<String>) -> Self {
208        let msg = msg.into();
209        Self::ResponseStructureError {
210            field: "unknown".to_string(),
211            context: msg,
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_error_is_retriable() {
222        assert!(YahooError::Timeout { timeout_ms: 5000 }.is_retriable());
223        assert!(YahooError::RateLimited { retry_after: None }.is_retriable());
224        assert!(
225            YahooError::AuthenticationFailed {
226                context: "test".to_string()
227            }
228            .is_retriable()
229        );
230        assert!(
231            YahooError::ServerError {
232                status: 500,
233                context: "test".to_string()
234            }
235            .is_retriable()
236        );
237        assert!(
238            !YahooError::SymbolNotFound {
239                symbol: Some("AAPL".to_string()),
240                context: "test".to_string()
241            }
242            .is_retriable()
243        );
244        assert!(
245            !YahooError::InvalidParameter {
246                param: "test".to_string(),
247                reason: "invalid".to_string()
248            }
249            .is_retriable()
250        );
251    }
252
253    #[test]
254    fn test_error_is_auth_error() {
255        assert!(
256            YahooError::AuthenticationFailed {
257                context: "test".to_string()
258            }
259            .is_auth_error()
260        );
261        assert!(!YahooError::Timeout { timeout_ms: 5000 }.is_auth_error());
262    }
263
264    #[test]
265    fn test_error_is_not_found() {
266        assert!(
267            YahooError::SymbolNotFound {
268                symbol: Some("AAPL".to_string()),
269                context: "test".to_string()
270            }
271            .is_not_found()
272        );
273        assert!(!YahooError::Timeout { timeout_ms: 5000 }.is_not_found());
274    }
275
276    #[test]
277    fn test_retry_after_secs() {
278        assert_eq!(
279            YahooError::RateLimited {
280                retry_after: Some(10)
281            }
282            .retry_after_secs(),
283            Some(10)
284        );
285        assert_eq!(
286            YahooError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
287            Some(2)
288        );
289        assert_eq!(
290            YahooError::ServerError {
291                status: 503,
292                context: "test".to_string()
293            }
294            .retry_after_secs(),
295            Some(5)
296        );
297        assert_eq!(
298            YahooError::SymbolNotFound {
299                symbol: None,
300                context: "test".to_string()
301            }
302            .retry_after_secs(),
303            None
304        );
305    }
306
307    #[test]
308    fn test_error_category() {
309        assert_eq!(
310            YahooError::AuthenticationFailed {
311                context: "test".to_string()
312            }
313            .category(),
314            ErrorCategory::Auth
315        );
316        assert_eq!(
317            YahooError::RateLimited { retry_after: None }.category(),
318            ErrorCategory::RateLimit
319        );
320        assert_eq!(
321            YahooError::Timeout { timeout_ms: 5000 }.category(),
322            ErrorCategory::Timeout
323        );
324        assert_eq!(
325            YahooError::SymbolNotFound {
326                symbol: None,
327                context: "test".to_string()
328            }
329            .category(),
330            ErrorCategory::NotFound
331        );
332    }
333
334    #[test]
335    fn test_with_symbol() {
336        let error = YahooError::SymbolNotFound {
337            symbol: None,
338            context: "test".to_string(),
339        }
340        .with_symbol("AAPL");
341
342        if let YahooError::SymbolNotFound { symbol, .. } = error {
343            assert_eq!(symbol, Some("AAPL".to_string()));
344        } else {
345            panic!("Expected SymbolNotFound");
346        }
347    }
348
349    #[test]
350    fn test_with_context() {
351        let error = YahooError::AuthenticationFailed {
352            context: "old".to_string(),
353        }
354        .with_context("new context");
355
356        if let YahooError::AuthenticationFailed { context } = error {
357            assert_eq!(context, "new context");
358        } else {
359            panic!("Expected AuthenticationFailed");
360        }
361    }
362}