1use crate::providers::{Capability, Operation, Provider};
2use thiserror::Error;
3
4fn join_providers(providers: &[Provider]) -> String {
5 providers
6 .iter()
7 .map(|p| p.as_str())
8 .collect::<Vec<_>>()
9 .join(", ")
10}
11
12#[derive(Error, Debug)]
14#[non_exhaustive]
15pub enum FinanceError {
16 #[error("Authentication failed: {context}")]
18 AuthenticationFailed {
19 context: String,
21 },
22
23 #[error("Symbol not found: {}", symbol.as_deref().unwrap_or("unknown"))]
25 SymbolNotFound {
26 symbol: Option<String>,
28 context: String,
30 },
31
32 #[error("Rate limited{}", match retry_after {
34 Some(s) => format!(" (retry after {s}s)"),
35 None => String::new(),
36 })]
37 RateLimited {
38 retry_after: Option<u64>,
40 },
41
42 #[error("HTTP request failed: {0}")]
44 HttpError(#[from] reqwest::Error),
45
46 #[error("Network request to {api} failed")]
52 NetworkError {
53 api: String,
55 },
56
57 #[error("JSON parse error: {0}")]
59 JsonParseError(#[from] serde_json::Error),
60
61 #[error("Response structure error in '{field}': {context}")]
63 ResponseStructureError {
64 field: String,
66 context: String,
68 },
69
70 #[error("Invalid parameter '{param}': {reason}")]
72 InvalidParameter {
73 param: String,
75 reason: String,
77 },
78
79 #[error("Request timeout after {timeout_ms}ms")]
81 Timeout {
82 timeout_ms: u64,
84 },
85
86 #[error("Server error {status}: {context}")]
88 ServerError {
89 status: u16,
91 context: String,
93 },
94
95 #[error("Unexpected response: {0}")]
97 UnexpectedResponse(String),
98
99 #[error("Internal error: {0}")]
101 InternalError(String),
102
103 #[error("API error: {0}")]
105 ApiError(String),
106
107 #[error("Runtime error: {0}")]
109 RuntimeError(#[from] std::io::Error),
110
111 #[cfg(feature = "indicators")]
113 #[error("Indicator calculation error: {0}")]
114 IndicatorError(#[from] crate::indicators::IndicatorError),
115
116 #[error("External API error from '{api}': HTTP {status}")]
118 ExternalApiError {
119 api: String,
121 status: u16,
123 },
124
125 #[error("Macro data error from '{provider}': {context}")]
127 MacroDataError {
128 provider: String,
130 context: String,
132 },
133
134 #[error("Feed parse error for '{url}': {context}")]
136 FeedParseError {
137 url: String,
139 context: String,
141 },
142
143 #[error(
145 "{provider} does not support {operation} (supported by: {}; route it via Providers::builder().route(...))",
146 join_providers(candidates)
147 )]
148 NotSupported {
149 provider: Provider,
151 operation: Operation,
153 candidates: Vec<Provider>,
156 },
157
158 #[error(
160 "no provider available for {operation} (supported by: {}; route it via Providers::builder().route(...))",
161 join_providers(candidates)
162 )]
163 NoProviderAvailable {
164 operation: Capability,
166 candidates: Vec<Provider>,
169 },
170
171 #[error(
173 "no adapter registered for {provider}; register it with \
174 ProvidersBuilder::with_adapter(..) before routing to it"
175 )]
176 ProviderNotRegistered {
177 provider: Provider,
179 },
180
181 #[cfg(feature = "translation")]
183 #[error("Translation error: {context}")]
184 TranslationError {
185 context: String,
187 },
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum ErrorCategory {
193 Auth,
195 RateLimit,
197 Timeout,
199 Server,
201 NotFound,
203 Validation,
205 Parsing,
207 Other,
209}
210
211pub type Result<T> = std::result::Result<T, FinanceError>;
213
214impl FinanceError {
215 pub fn is_retriable(&self) -> bool {
217 matches!(
218 self,
219 FinanceError::Timeout { .. }
220 | FinanceError::RateLimited { .. }
221 | FinanceError::HttpError(_)
222 | FinanceError::NetworkError { .. }
223 | FinanceError::AuthenticationFailed { .. }
224 | FinanceError::ServerError { .. }
225 ) || matches!(self, FinanceError::ExternalApiError { status, .. } if *status >= 500)
226 }
227
228 pub fn is_auth_error(&self) -> bool {
230 matches!(self, FinanceError::AuthenticationFailed { .. })
231 }
232
233 pub fn is_not_found(&self) -> bool {
235 matches!(self, FinanceError::SymbolNotFound { .. })
236 }
237
238 pub fn retry_after_secs(&self) -> Option<u64> {
240 match self {
241 Self::RateLimited { retry_after } => *retry_after,
242 Self::Timeout { .. } => Some(2),
243 Self::ServerError { status, .. } if *status >= 500 => Some(5),
244 Self::AuthenticationFailed { .. } => Some(1),
245 _ => None,
246 }
247 }
248
249 pub fn category(&self) -> ErrorCategory {
251 match self {
252 Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
253 Self::RateLimited { .. } => ErrorCategory::RateLimit,
254 Self::Timeout { .. } => ErrorCategory::Timeout,
255 Self::ServerError { .. } => ErrorCategory::Server,
256 Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
257 Self::InvalidParameter { .. } => ErrorCategory::Validation,
258 Self::JsonParseError(_)
259 | Self::ResponseStructureError { .. }
260 | Self::MacroDataError { .. }
261 | Self::FeedParseError { .. } => ErrorCategory::Parsing,
262 Self::NotSupported { .. } | Self::NoProviderAvailable { .. } => {
263 ErrorCategory::Validation
264 }
265 Self::ExternalApiError { .. } => ErrorCategory::Server,
266 _ => ErrorCategory::Other,
267 }
268 }
269
270 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
272 if let Self::SymbolNotFound {
273 symbol: ref mut s, ..
274 } = self
275 {
276 *s = Some(symbol.into());
277 }
278 self
279 }
280
281 pub fn with_context(mut self, context: impl Into<String>) -> Self {
283 match self {
284 Self::AuthenticationFailed {
285 context: ref mut c, ..
286 } => {
287 *c = context.into();
288 }
289 Self::SymbolNotFound {
290 context: ref mut c, ..
291 } => {
292 *c = context.into();
293 }
294 Self::ResponseStructureError {
295 context: ref mut c, ..
296 } => {
297 *c = context.into();
298 }
299 Self::ServerError {
300 context: ref mut c, ..
301 } => {
302 *c = context.into();
303 }
304 Self::MacroDataError {
305 context: ref mut c, ..
306 } => {
307 *c = context.into();
308 }
309 Self::FeedParseError {
310 context: ref mut c, ..
311 } => {
312 *c = context.into();
313 }
314 _ => {}
315 }
316 self
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_error_is_retriable() {
326 assert!(FinanceError::Timeout { timeout_ms: 5000 }.is_retriable());
327 assert!(FinanceError::RateLimited { retry_after: None }.is_retriable());
328 assert!(
329 FinanceError::AuthenticationFailed {
330 context: "test".to_string()
331 }
332 .is_retriable()
333 );
334 assert!(
335 FinanceError::ServerError {
336 status: 500,
337 context: "test".to_string()
338 }
339 .is_retriable()
340 );
341 assert!(
342 !FinanceError::SymbolNotFound {
343 symbol: Some("AAPL".to_string()),
344 context: "test".to_string()
345 }
346 .is_retriable()
347 );
348 assert!(
349 !FinanceError::InvalidParameter {
350 param: "test".to_string(),
351 reason: "invalid".to_string()
352 }
353 .is_retriable()
354 );
355 }
356
357 #[test]
358 fn test_error_is_auth_error() {
359 assert!(
360 FinanceError::AuthenticationFailed {
361 context: "test".to_string()
362 }
363 .is_auth_error()
364 );
365 assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_auth_error());
366 }
367
368 #[test]
369 fn test_error_is_not_found() {
370 assert!(
371 FinanceError::SymbolNotFound {
372 symbol: Some("AAPL".to_string()),
373 context: "test".to_string()
374 }
375 .is_not_found()
376 );
377 assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_not_found());
378 }
379
380 #[test]
381 fn test_retry_after_secs() {
382 assert_eq!(
383 FinanceError::RateLimited {
384 retry_after: Some(10)
385 }
386 .retry_after_secs(),
387 Some(10)
388 );
389 assert_eq!(
390 FinanceError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
391 Some(2)
392 );
393 assert_eq!(
394 FinanceError::ServerError {
395 status: 503,
396 context: "test".to_string()
397 }
398 .retry_after_secs(),
399 Some(5)
400 );
401 assert_eq!(
402 FinanceError::SymbolNotFound {
403 symbol: None,
404 context: "test".to_string()
405 }
406 .retry_after_secs(),
407 None
408 );
409 }
410
411 #[test]
412 fn test_error_category() {
413 assert_eq!(
414 FinanceError::AuthenticationFailed {
415 context: "test".to_string()
416 }
417 .category(),
418 ErrorCategory::Auth
419 );
420 assert_eq!(
421 FinanceError::RateLimited { retry_after: None }.category(),
422 ErrorCategory::RateLimit
423 );
424 assert_eq!(
425 FinanceError::Timeout { timeout_ms: 5000 }.category(),
426 ErrorCategory::Timeout
427 );
428 assert_eq!(
429 FinanceError::SymbolNotFound {
430 symbol: None,
431 context: "test".to_string()
432 }
433 .category(),
434 ErrorCategory::NotFound
435 );
436 }
437
438 #[test]
439 fn test_with_symbol() {
440 let error = FinanceError::SymbolNotFound {
441 symbol: None,
442 context: "test".to_string(),
443 }
444 .with_symbol("AAPL");
445
446 if let FinanceError::SymbolNotFound { symbol, .. } = error {
447 assert_eq!(symbol, Some("AAPL".to_string()));
448 } else {
449 panic!("Expected SymbolNotFound");
450 }
451 }
452
453 #[test]
454 fn test_with_context() {
455 let error = FinanceError::AuthenticationFailed {
456 context: "old".to_string(),
457 }
458 .with_context("new context");
459
460 if let FinanceError::AuthenticationFailed { context } = error {
461 assert_eq!(context, "new context");
462 } else {
463 panic!("Expected AuthenticationFailed");
464 }
465 }
466}