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
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ErrorCategory {
139 Auth,
141 RateLimit,
143 Timeout,
145 Server,
147 NotFound,
149 Validation,
151 Parsing,
153 Other,
155}
156
157pub type Result<T> = std::result::Result<T, FinanceError>;
159
160impl FinanceError {
161 pub fn is_retriable(&self) -> bool {
163 matches!(
164 self,
165 FinanceError::Timeout { .. }
166 | FinanceError::RateLimited { .. }
167 | FinanceError::HttpError(_)
168 | FinanceError::AuthenticationFailed { .. }
169 | FinanceError::ServerError { .. }
170 )
171 }
172
173 pub fn is_auth_error(&self) -> bool {
175 matches!(self, FinanceError::AuthenticationFailed { .. })
176 }
177
178 pub fn is_not_found(&self) -> bool {
180 matches!(self, FinanceError::SymbolNotFound { .. })
181 }
182
183 pub fn retry_after_secs(&self) -> Option<u64> {
185 match self {
186 Self::RateLimited { retry_after } => *retry_after,
187 Self::Timeout { .. } => Some(2),
188 Self::ServerError { status, .. } if *status >= 500 => Some(5),
189 Self::AuthenticationFailed { .. } => Some(1),
190 _ => None,
191 }
192 }
193
194 pub fn category(&self) -> ErrorCategory {
196 match self {
197 Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
198 Self::RateLimited { .. } => ErrorCategory::RateLimit,
199 Self::Timeout { .. } => ErrorCategory::Timeout,
200 Self::ServerError { .. } => ErrorCategory::Server,
201 Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
202 Self::InvalidParameter { .. } => ErrorCategory::Validation,
203 Self::JsonParseError(_) | Self::ResponseStructureError { .. } => ErrorCategory::Parsing,
204 Self::NotSupported { .. } | Self::NoProviderAvailable { .. } => {
205 ErrorCategory::Validation
206 }
207 _ => ErrorCategory::Other,
208 }
209 }
210
211 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
213 if let Self::SymbolNotFound {
214 symbol: ref mut s, ..
215 } = self
216 {
217 *s = Some(symbol.into());
218 }
219 self
220 }
221
222 pub fn with_context(mut self, context: impl Into<String>) -> Self {
224 match self {
225 Self::AuthenticationFailed {
226 context: ref mut c, ..
227 } => {
228 *c = context.into();
229 }
230 Self::SymbolNotFound {
231 context: ref mut c, ..
232 } => {
233 *c = context.into();
234 }
235 Self::ResponseStructureError {
236 context: ref mut c, ..
237 } => {
238 *c = context.into();
239 }
240 Self::ServerError {
241 context: ref mut c, ..
242 } => {
243 *c = context.into();
244 }
245 _ => {}
246 }
247 self
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_error_is_retriable() {
257 assert!(FinanceError::Timeout { timeout_ms: 5000 }.is_retriable());
258 assert!(FinanceError::RateLimited { retry_after: None }.is_retriable());
259 assert!(
260 FinanceError::AuthenticationFailed {
261 context: "test".to_string()
262 }
263 .is_retriable()
264 );
265 assert!(
266 FinanceError::ServerError {
267 status: 500,
268 context: "test".to_string()
269 }
270 .is_retriable()
271 );
272 assert!(
273 !FinanceError::SymbolNotFound {
274 symbol: Some("AAPL".to_string()),
275 context: "test".to_string()
276 }
277 .is_retriable()
278 );
279 assert!(
280 !FinanceError::InvalidParameter {
281 param: "test".to_string(),
282 reason: "invalid".to_string()
283 }
284 .is_retriable()
285 );
286 }
287
288 #[test]
289 fn test_error_is_auth_error() {
290 assert!(
291 FinanceError::AuthenticationFailed {
292 context: "test".to_string()
293 }
294 .is_auth_error()
295 );
296 assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_auth_error());
297 }
298
299 #[test]
300 fn test_error_is_not_found() {
301 assert!(
302 FinanceError::SymbolNotFound {
303 symbol: Some("AAPL".to_string()),
304 context: "test".to_string()
305 }
306 .is_not_found()
307 );
308 assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_not_found());
309 }
310
311 #[test]
312 fn test_retry_after_secs() {
313 assert_eq!(
314 FinanceError::RateLimited {
315 retry_after: Some(10)
316 }
317 .retry_after_secs(),
318 Some(10)
319 );
320 assert_eq!(
321 FinanceError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
322 Some(2)
323 );
324 assert_eq!(
325 FinanceError::ServerError {
326 status: 503,
327 context: "test".to_string()
328 }
329 .retry_after_secs(),
330 Some(5)
331 );
332 assert_eq!(
333 FinanceError::SymbolNotFound {
334 symbol: None,
335 context: "test".to_string()
336 }
337 .retry_after_secs(),
338 None
339 );
340 }
341
342 #[test]
343 fn test_error_category() {
344 assert_eq!(
345 FinanceError::AuthenticationFailed {
346 context: "test".to_string()
347 }
348 .category(),
349 ErrorCategory::Auth
350 );
351 assert_eq!(
352 FinanceError::RateLimited { retry_after: None }.category(),
353 ErrorCategory::RateLimit
354 );
355 assert_eq!(
356 FinanceError::Timeout { timeout_ms: 5000 }.category(),
357 ErrorCategory::Timeout
358 );
359 assert_eq!(
360 FinanceError::SymbolNotFound {
361 symbol: None,
362 context: "test".to_string()
363 }
364 .category(),
365 ErrorCategory::NotFound
366 );
367 }
368
369 #[test]
370 fn test_with_symbol() {
371 let error = FinanceError::SymbolNotFound {
372 symbol: None,
373 context: "test".to_string(),
374 }
375 .with_symbol("AAPL");
376
377 if let FinanceError::SymbolNotFound { symbol, .. } = error {
378 assert_eq!(symbol, Some("AAPL".to_string()));
379 } else {
380 panic!("Expected SymbolNotFound");
381 }
382 }
383
384 #[test]
385 fn test_with_context() {
386 let error = FinanceError::AuthenticationFailed {
387 context: "old".to_string(),
388 }
389 .with_context("new context");
390
391 if let FinanceError::AuthenticationFailed { context } = error {
392 assert_eq!(context, "new context");
393 } else {
394 panic!("Expected AuthenticationFailed");
395 }
396 }
397}