Skip to main content

finance_query/
error.rs

1use thiserror::Error;
2
3/// Main error type for the library
4#[derive(Error, Debug)]
5pub enum FinanceError {
6    /// Authentication failed (Yahoo Finance, SEC EDGAR, etc.)
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 API response
72    #[error("Unexpected response: {0}")]
73    UnexpectedResponse(String),
74
75    /// Internal error
76    #[error("Internal error: {0}")]
77    InternalError(String),
78
79    /// General API error
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    /// Indicator calculation error
88    #[cfg(feature = "indicators")]
89    #[error("Indicator calculation error: {0}")]
90    IndicatorError(#[from] crate::indicators::IndicatorError),
91
92    /// Error from an external (non-Yahoo) data API
93    #[error("External API error from '{api}': HTTP {status}")]
94    ExternalApiError {
95        /// Name of the external API (e.g., "alternative.me", "coingecko")
96        api: String,
97        /// HTTP status code returned
98        status: u16,
99    },
100
101    /// Error fetching or parsing macro-economic data (FRED, Treasury, BLS)
102    #[error("Macro data error from '{provider}': {context}")]
103    MacroDataError {
104        /// Provider name (e.g., "FRED", "US Treasury")
105        provider: String,
106        /// Error context
107        context: String,
108    },
109
110    /// Error parsing an RSS/Atom feed
111    #[error("Feed parse error for '{url}': {context}")]
112    FeedParseError {
113        /// Feed URL that failed
114        url: String,
115        /// Error context
116        context: String,
117    },
118}
119
120/// Error category for logging and metrics
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum ErrorCategory {
123    /// Authentication errors
124    Auth,
125    /// Rate limiting errors
126    RateLimit,
127    /// Timeout errors
128    Timeout,
129    /// Server errors (5xx)
130    Server,
131    /// Not found errors
132    NotFound,
133    /// Validation errors
134    Validation,
135    /// Parsing errors
136    Parsing,
137    /// Other errors
138    Other,
139}
140
141/// Type alias for Error (for consistency with common Rust patterns)
142pub type Error = FinanceError;
143
144/// Result type alias for library operations
145pub type Result<T> = std::result::Result<T, FinanceError>;
146
147impl FinanceError {
148    /// Check if this error is retriable
149    pub fn is_retriable(&self) -> bool {
150        matches!(
151            self,
152            FinanceError::Timeout { .. }
153                | FinanceError::RateLimited { .. }
154                | FinanceError::HttpError(_)
155                | FinanceError::AuthenticationFailed { .. }
156                | FinanceError::ServerError { .. }
157        )
158    }
159
160    /// Check if this error indicates an authentication issue
161    pub fn is_auth_error(&self) -> bool {
162        matches!(self, FinanceError::AuthenticationFailed { .. })
163    }
164
165    /// Check if this error indicates a not found issue
166    pub fn is_not_found(&self) -> bool {
167        matches!(self, FinanceError::SymbolNotFound { .. })
168    }
169
170    /// Get retry delay in seconds (for exponential backoff)
171    pub fn retry_after_secs(&self) -> Option<u64> {
172        match self {
173            Self::RateLimited { retry_after } => *retry_after,
174            Self::Timeout { .. } => Some(2),
175            Self::ServerError { status, .. } if *status >= 500 => Some(5),
176            Self::AuthenticationFailed { .. } => Some(1),
177            _ => None,
178        }
179    }
180
181    /// Categorize errors for logging/metrics
182    pub fn category(&self) -> ErrorCategory {
183        match self {
184            Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
185            Self::RateLimited { .. } => ErrorCategory::RateLimit,
186            Self::Timeout { .. } => ErrorCategory::Timeout,
187            Self::ServerError { .. } => ErrorCategory::Server,
188            Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
189            Self::InvalidParameter { .. } => ErrorCategory::Validation,
190            Self::JsonParseError(_) | Self::ResponseStructureError { .. } => ErrorCategory::Parsing,
191            _ => ErrorCategory::Other,
192        }
193    }
194
195    /// Add symbol context to error (fluent API)
196    pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
197        if let Self::SymbolNotFound {
198            symbol: ref mut s, ..
199        } = self
200        {
201            *s = Some(symbol.into());
202        }
203        self
204    }
205
206    /// Add context to error (fluent API)
207    pub fn with_context(mut self, context: impl Into<String>) -> Self {
208        match self {
209            Self::AuthenticationFailed {
210                context: ref mut c, ..
211            } => {
212                *c = context.into();
213            }
214            Self::SymbolNotFound {
215                context: ref mut c, ..
216            } => {
217                *c = context.into();
218            }
219            Self::ResponseStructureError {
220                context: ref mut c, ..
221            } => {
222                *c = context.into();
223            }
224            Self::ServerError {
225                context: ref mut c, ..
226            } => {
227                *c = context.into();
228            }
229            _ => {}
230        }
231        self
232    }
233}
234
235// Backward compatibility: Allow ParseError to be created from String
236impl FinanceError {
237    /// Create a ParseError from a string (for backward compatibility)
238    #[deprecated(since = "2.0.0", note = "Use ResponseStructureError instead")]
239    pub fn parse_error(msg: impl Into<String>) -> Self {
240        let msg = msg.into();
241        Self::ResponseStructureError {
242            field: "unknown".to_string(),
243            context: msg,
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_error_is_retriable() {
254        assert!(FinanceError::Timeout { timeout_ms: 5000 }.is_retriable());
255        assert!(FinanceError::RateLimited { retry_after: None }.is_retriable());
256        assert!(
257            FinanceError::AuthenticationFailed {
258                context: "test".to_string()
259            }
260            .is_retriable()
261        );
262        assert!(
263            FinanceError::ServerError {
264                status: 500,
265                context: "test".to_string()
266            }
267            .is_retriable()
268        );
269        assert!(
270            !FinanceError::SymbolNotFound {
271                symbol: Some("AAPL".to_string()),
272                context: "test".to_string()
273            }
274            .is_retriable()
275        );
276        assert!(
277            !FinanceError::InvalidParameter {
278                param: "test".to_string(),
279                reason: "invalid".to_string()
280            }
281            .is_retriable()
282        );
283    }
284
285    #[test]
286    fn test_error_is_auth_error() {
287        assert!(
288            FinanceError::AuthenticationFailed {
289                context: "test".to_string()
290            }
291            .is_auth_error()
292        );
293        assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_auth_error());
294    }
295
296    #[test]
297    fn test_error_is_not_found() {
298        assert!(
299            FinanceError::SymbolNotFound {
300                symbol: Some("AAPL".to_string()),
301                context: "test".to_string()
302            }
303            .is_not_found()
304        );
305        assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_not_found());
306    }
307
308    #[test]
309    fn test_retry_after_secs() {
310        assert_eq!(
311            FinanceError::RateLimited {
312                retry_after: Some(10)
313            }
314            .retry_after_secs(),
315            Some(10)
316        );
317        assert_eq!(
318            FinanceError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
319            Some(2)
320        );
321        assert_eq!(
322            FinanceError::ServerError {
323                status: 503,
324                context: "test".to_string()
325            }
326            .retry_after_secs(),
327            Some(5)
328        );
329        assert_eq!(
330            FinanceError::SymbolNotFound {
331                symbol: None,
332                context: "test".to_string()
333            }
334            .retry_after_secs(),
335            None
336        );
337    }
338
339    #[test]
340    fn test_error_category() {
341        assert_eq!(
342            FinanceError::AuthenticationFailed {
343                context: "test".to_string()
344            }
345            .category(),
346            ErrorCategory::Auth
347        );
348        assert_eq!(
349            FinanceError::RateLimited { retry_after: None }.category(),
350            ErrorCategory::RateLimit
351        );
352        assert_eq!(
353            FinanceError::Timeout { timeout_ms: 5000 }.category(),
354            ErrorCategory::Timeout
355        );
356        assert_eq!(
357            FinanceError::SymbolNotFound {
358                symbol: None,
359                context: "test".to_string()
360            }
361            .category(),
362            ErrorCategory::NotFound
363        );
364    }
365
366    #[test]
367    fn test_with_symbol() {
368        let error = FinanceError::SymbolNotFound {
369            symbol: None,
370            context: "test".to_string(),
371        }
372        .with_symbol("AAPL");
373
374        if let FinanceError::SymbolNotFound { symbol, .. } = error {
375            assert_eq!(symbol, Some("AAPL".to_string()));
376        } else {
377            panic!("Expected SymbolNotFound");
378        }
379    }
380
381    #[test]
382    fn test_with_context() {
383        let error = FinanceError::AuthenticationFailed {
384            context: "old".to_string(),
385        }
386        .with_context("new context");
387
388        if let FinanceError::AuthenticationFailed { context } = error {
389            assert_eq!(context, "new context");
390        } else {
391            panic!("Expected AuthenticationFailed");
392        }
393    }
394}