1use thiserror::Error;
2
3#[derive(Error, Debug)]
5pub enum FinanceError {
6 #[error("Authentication failed: {context}")]
8 AuthenticationFailed {
9 context: String,
11 },
12
13 #[error("Symbol not found: {}", symbol.as_deref().unwrap_or("unknown"))]
15 SymbolNotFound {
16 symbol: Option<String>,
18 context: String,
20 },
21
22 #[error("Rate limited (retry after {retry_after:?}s)")]
24 RateLimited {
25 retry_after: Option<u64>,
27 },
28
29 #[error("HTTP request failed: {0}")]
31 HttpError(#[from] reqwest::Error),
32
33 #[error("JSON parse error: {0}")]
35 JsonParseError(#[from] serde_json::Error),
36
37 #[error("Response structure error in '{field}': {context}")]
39 ResponseStructureError {
40 field: String,
42 context: String,
44 },
45
46 #[error("Invalid parameter '{param}': {reason}")]
48 InvalidParameter {
49 param: String,
51 reason: String,
53 },
54
55 #[error("Request timeout after {timeout_ms}ms")]
57 Timeout {
58 timeout_ms: u64,
60 },
61
62 #[error("Server error {status}: {context}")]
64 ServerError {
65 status: u16,
67 context: String,
69 },
70
71 #[error("Unexpected response: {0}")]
73 UnexpectedResponse(String),
74
75 #[error("Internal error: {0}")]
77 InternalError(String),
78
79 #[error("API error: {0}")]
81 ApiError(String),
82
83 #[error("Runtime error: {0}")]
85 RuntimeError(#[from] std::io::Error),
86
87 #[cfg(feature = "indicators")]
89 #[error("Indicator calculation error: {0}")]
90 IndicatorError(#[from] crate::indicators::IndicatorError),
91
92 #[error("External API error from '{api}': HTTP {status}")]
94 ExternalApiError {
95 api: String,
97 status: u16,
99 },
100
101 #[error("Macro data error from '{provider}': {context}")]
103 MacroDataError {
104 provider: String,
106 context: String,
108 },
109
110 #[error("Feed parse error for '{url}': {context}")]
112 FeedParseError {
113 url: String,
115 context: String,
117 },
118
119 #[error("{provider} does not support {operation}")]
121 NotSupported {
122 provider: &'static str,
124 operation: &'static str,
126 },
127
128 #[error("no provider available for {operation}")]
130 NoProviderAvailable {
131 operation: &'static str,
133 },
134
135 #[cfg(feature = "translation")]
137 #[error("Translation error: {context}")]
138 TranslationError {
139 context: String,
141 },
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ErrorCategory {
147 Auth,
149 RateLimit,
151 Timeout,
153 Server,
155 NotFound,
157 Validation,
159 Parsing,
161 Other,
163}
164
165pub type Result<T> = std::result::Result<T, FinanceError>;
167
168impl FinanceError {
169 pub fn is_retriable(&self) -> bool {
171 matches!(
172 self,
173 FinanceError::Timeout { .. }
174 | FinanceError::RateLimited { .. }
175 | FinanceError::HttpError(_)
176 | FinanceError::AuthenticationFailed { .. }
177 | FinanceError::ServerError { .. }
178 )
179 }
180
181 pub fn is_auth_error(&self) -> bool {
183 matches!(self, FinanceError::AuthenticationFailed { .. })
184 }
185
186 pub fn is_not_found(&self) -> bool {
188 matches!(self, FinanceError::SymbolNotFound { .. })
189 }
190
191 pub fn retry_after_secs(&self) -> Option<u64> {
193 match self {
194 Self::RateLimited { retry_after } => *retry_after,
195 Self::Timeout { .. } => Some(2),
196 Self::ServerError { status, .. } if *status >= 500 => Some(5),
197 Self::AuthenticationFailed { .. } => Some(1),
198 _ => None,
199 }
200 }
201
202 pub fn category(&self) -> ErrorCategory {
204 match self {
205 Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
206 Self::RateLimited { .. } => ErrorCategory::RateLimit,
207 Self::Timeout { .. } => ErrorCategory::Timeout,
208 Self::ServerError { .. } => ErrorCategory::Server,
209 Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
210 Self::InvalidParameter { .. } => ErrorCategory::Validation,
211 Self::JsonParseError(_) | Self::ResponseStructureError { .. } => ErrorCategory::Parsing,
212 Self::NotSupported { .. } | Self::NoProviderAvailable { .. } => {
213 ErrorCategory::Validation
214 }
215 _ => ErrorCategory::Other,
216 }
217 }
218
219 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
221 if let Self::SymbolNotFound {
222 symbol: ref mut s, ..
223 } = self
224 {
225 *s = Some(symbol.into());
226 }
227 self
228 }
229
230 pub fn with_context(mut self, context: impl Into<String>) -> Self {
232 match self {
233 Self::AuthenticationFailed {
234 context: ref mut c, ..
235 } => {
236 *c = context.into();
237 }
238 Self::SymbolNotFound {
239 context: ref mut c, ..
240 } => {
241 *c = context.into();
242 }
243 Self::ResponseStructureError {
244 context: ref mut c, ..
245 } => {
246 *c = context.into();
247 }
248 Self::ServerError {
249 context: ref mut c, ..
250 } => {
251 *c = context.into();
252 }
253 _ => {}
254 }
255 self
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_error_is_retriable() {
265 assert!(FinanceError::Timeout { timeout_ms: 5000 }.is_retriable());
266 assert!(FinanceError::RateLimited { retry_after: None }.is_retriable());
267 assert!(
268 FinanceError::AuthenticationFailed {
269 context: "test".to_string()
270 }
271 .is_retriable()
272 );
273 assert!(
274 FinanceError::ServerError {
275 status: 500,
276 context: "test".to_string()
277 }
278 .is_retriable()
279 );
280 assert!(
281 !FinanceError::SymbolNotFound {
282 symbol: Some("AAPL".to_string()),
283 context: "test".to_string()
284 }
285 .is_retriable()
286 );
287 assert!(
288 !FinanceError::InvalidParameter {
289 param: "test".to_string(),
290 reason: "invalid".to_string()
291 }
292 .is_retriable()
293 );
294 }
295
296 #[test]
297 fn test_error_is_auth_error() {
298 assert!(
299 FinanceError::AuthenticationFailed {
300 context: "test".to_string()
301 }
302 .is_auth_error()
303 );
304 assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_auth_error());
305 }
306
307 #[test]
308 fn test_error_is_not_found() {
309 assert!(
310 FinanceError::SymbolNotFound {
311 symbol: Some("AAPL".to_string()),
312 context: "test".to_string()
313 }
314 .is_not_found()
315 );
316 assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_not_found());
317 }
318
319 #[test]
320 fn test_retry_after_secs() {
321 assert_eq!(
322 FinanceError::RateLimited {
323 retry_after: Some(10)
324 }
325 .retry_after_secs(),
326 Some(10)
327 );
328 assert_eq!(
329 FinanceError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
330 Some(2)
331 );
332 assert_eq!(
333 FinanceError::ServerError {
334 status: 503,
335 context: "test".to_string()
336 }
337 .retry_after_secs(),
338 Some(5)
339 );
340 assert_eq!(
341 FinanceError::SymbolNotFound {
342 symbol: None,
343 context: "test".to_string()
344 }
345 .retry_after_secs(),
346 None
347 );
348 }
349
350 #[test]
351 fn test_error_category() {
352 assert_eq!(
353 FinanceError::AuthenticationFailed {
354 context: "test".to_string()
355 }
356 .category(),
357 ErrorCategory::Auth
358 );
359 assert_eq!(
360 FinanceError::RateLimited { retry_after: None }.category(),
361 ErrorCategory::RateLimit
362 );
363 assert_eq!(
364 FinanceError::Timeout { timeout_ms: 5000 }.category(),
365 ErrorCategory::Timeout
366 );
367 assert_eq!(
368 FinanceError::SymbolNotFound {
369 symbol: None,
370 context: "test".to_string()
371 }
372 .category(),
373 ErrorCategory::NotFound
374 );
375 }
376
377 #[test]
378 fn test_with_symbol() {
379 let error = FinanceError::SymbolNotFound {
380 symbol: None,
381 context: "test".to_string(),
382 }
383 .with_symbol("AAPL");
384
385 if let FinanceError::SymbolNotFound { symbol, .. } = error {
386 assert_eq!(symbol, Some("AAPL".to_string()));
387 } else {
388 panic!("Expected SymbolNotFound");
389 }
390 }
391
392 #[test]
393 fn test_with_context() {
394 let error = FinanceError::AuthenticationFailed {
395 context: "old".to_string(),
396 }
397 .with_context("new context");
398
399 if let FinanceError::AuthenticationFailed { context } = error {
400 assert_eq!(context, "new context");
401 } else {
402 panic!("Expected AuthenticationFailed");
403 }
404 }
405}