1use thiserror::Error;
2
3#[derive(Error, Debug)]
5pub enum YahooError {
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
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum ErrorCategory {
91 Auth,
93 RateLimit,
95 Timeout,
97 Server,
99 NotFound,
101 Validation,
103 Parsing,
105 Other,
107}
108
109pub type Error = YahooError;
111
112pub type Result<T> = std::result::Result<T, YahooError>;
114
115impl YahooError {
116 pub fn is_retriable(&self) -> bool {
118 matches!(
119 self,
120 YahooError::Timeout { .. }
121 | YahooError::RateLimited { .. }
122 | YahooError::HttpError(_)
123 | YahooError::AuthenticationFailed { .. }
124 | YahooError::ServerError { .. }
125 )
126 }
127
128 pub fn is_auth_error(&self) -> bool {
130 matches!(self, YahooError::AuthenticationFailed { .. })
131 }
132
133 pub fn is_not_found(&self) -> bool {
135 matches!(self, YahooError::SymbolNotFound { .. })
136 }
137
138 pub fn retry_after_secs(&self) -> Option<u64> {
140 match self {
141 Self::RateLimited { retry_after } => *retry_after,
142 Self::Timeout { .. } => Some(2),
143 Self::ServerError { status, .. } if *status >= 500 => Some(5),
144 Self::AuthenticationFailed { .. } => Some(1),
145 _ => None,
146 }
147 }
148
149 pub fn category(&self) -> ErrorCategory {
151 match self {
152 Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
153 Self::RateLimited { .. } => ErrorCategory::RateLimit,
154 Self::Timeout { .. } => ErrorCategory::Timeout,
155 Self::ServerError { .. } => ErrorCategory::Server,
156 Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
157 Self::InvalidParameter { .. } => ErrorCategory::Validation,
158 Self::JsonParseError(_) | Self::ResponseStructureError { .. } => ErrorCategory::Parsing,
159 _ => ErrorCategory::Other,
160 }
161 }
162
163 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
165 if let Self::SymbolNotFound {
166 symbol: ref mut s, ..
167 } = self
168 {
169 *s = Some(symbol.into());
170 }
171 self
172 }
173
174 pub fn with_context(mut self, context: impl Into<String>) -> Self {
176 match self {
177 Self::AuthenticationFailed {
178 context: ref mut c, ..
179 } => {
180 *c = context.into();
181 }
182 Self::SymbolNotFound {
183 context: ref mut c, ..
184 } => {
185 *c = context.into();
186 }
187 Self::ResponseStructureError {
188 context: ref mut c, ..
189 } => {
190 *c = context.into();
191 }
192 Self::ServerError {
193 context: ref mut c, ..
194 } => {
195 *c = context.into();
196 }
197 _ => {}
198 }
199 self
200 }
201}
202
203impl YahooError {
205 #[deprecated(since = "2.0.0", note = "Use ResponseStructureError instead")]
207 pub fn parse_error(msg: impl Into<String>) -> Self {
208 let msg = msg.into();
209 Self::ResponseStructureError {
210 field: "unknown".to_string(),
211 context: msg,
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_error_is_retriable() {
222 assert!(YahooError::Timeout { timeout_ms: 5000 }.is_retriable());
223 assert!(YahooError::RateLimited { retry_after: None }.is_retriable());
224 assert!(
225 YahooError::AuthenticationFailed {
226 context: "test".to_string()
227 }
228 .is_retriable()
229 );
230 assert!(
231 YahooError::ServerError {
232 status: 500,
233 context: "test".to_string()
234 }
235 .is_retriable()
236 );
237 assert!(
238 !YahooError::SymbolNotFound {
239 symbol: Some("AAPL".to_string()),
240 context: "test".to_string()
241 }
242 .is_retriable()
243 );
244 assert!(
245 !YahooError::InvalidParameter {
246 param: "test".to_string(),
247 reason: "invalid".to_string()
248 }
249 .is_retriable()
250 );
251 }
252
253 #[test]
254 fn test_error_is_auth_error() {
255 assert!(
256 YahooError::AuthenticationFailed {
257 context: "test".to_string()
258 }
259 .is_auth_error()
260 );
261 assert!(!YahooError::Timeout { timeout_ms: 5000 }.is_auth_error());
262 }
263
264 #[test]
265 fn test_error_is_not_found() {
266 assert!(
267 YahooError::SymbolNotFound {
268 symbol: Some("AAPL".to_string()),
269 context: "test".to_string()
270 }
271 .is_not_found()
272 );
273 assert!(!YahooError::Timeout { timeout_ms: 5000 }.is_not_found());
274 }
275
276 #[test]
277 fn test_retry_after_secs() {
278 assert_eq!(
279 YahooError::RateLimited {
280 retry_after: Some(10)
281 }
282 .retry_after_secs(),
283 Some(10)
284 );
285 assert_eq!(
286 YahooError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
287 Some(2)
288 );
289 assert_eq!(
290 YahooError::ServerError {
291 status: 503,
292 context: "test".to_string()
293 }
294 .retry_after_secs(),
295 Some(5)
296 );
297 assert_eq!(
298 YahooError::SymbolNotFound {
299 symbol: None,
300 context: "test".to_string()
301 }
302 .retry_after_secs(),
303 None
304 );
305 }
306
307 #[test]
308 fn test_error_category() {
309 assert_eq!(
310 YahooError::AuthenticationFailed {
311 context: "test".to_string()
312 }
313 .category(),
314 ErrorCategory::Auth
315 );
316 assert_eq!(
317 YahooError::RateLimited { retry_after: None }.category(),
318 ErrorCategory::RateLimit
319 );
320 assert_eq!(
321 YahooError::Timeout { timeout_ms: 5000 }.category(),
322 ErrorCategory::Timeout
323 );
324 assert_eq!(
325 YahooError::SymbolNotFound {
326 symbol: None,
327 context: "test".to_string()
328 }
329 .category(),
330 ErrorCategory::NotFound
331 );
332 }
333
334 #[test]
335 fn test_with_symbol() {
336 let error = YahooError::SymbolNotFound {
337 symbol: None,
338 context: "test".to_string(),
339 }
340 .with_symbol("AAPL");
341
342 if let YahooError::SymbolNotFound { symbol, .. } = error {
343 assert_eq!(symbol, Some("AAPL".to_string()));
344 } else {
345 panic!("Expected SymbolNotFound");
346 }
347 }
348
349 #[test]
350 fn test_with_context() {
351 let error = YahooError::AuthenticationFailed {
352 context: "old".to_string(),
353 }
354 .with_context("new context");
355
356 if let YahooError::AuthenticationFailed { context } = error {
357 assert_eq!(context, "new context");
358 } else {
359 panic!("Expected AuthenticationFailed");
360 }
361 }
362}