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_ref().map(|s| s.as_str()).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
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum ErrorCategory {
123 Auth,
125 RateLimit,
127 Timeout,
129 Server,
131 NotFound,
133 Validation,
135 Parsing,
137 Other,
139}
140
141pub type Error = FinanceError;
143
144pub type Result<T> = std::result::Result<T, FinanceError>;
146
147impl FinanceError {
148 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 pub fn is_auth_error(&self) -> bool {
162 matches!(self, FinanceError::AuthenticationFailed { .. })
163 }
164
165 pub fn is_not_found(&self) -> bool {
167 matches!(self, FinanceError::SymbolNotFound { .. })
168 }
169
170 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 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 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 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
235impl FinanceError {
237 #[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}