1use std::result;
2
3use crate::retry::RetryableError;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum PubMedError {
9 #[error("HTTP request failed: {0}")]
11 RequestError(#[from] reqwest::Error),
12
13 #[error("JSON parsing failed: {0}")]
15 JsonError(#[from] serde_json::Error),
16
17 #[error("XML parsing failed: {0}")]
19 XmlError(String),
20
21 #[error("Article not found: PMID {pmid}")]
23 ArticleNotFound { pmid: String },
24
25 #[error("PMC full text not available for {id}")]
27 PmcNotAvailable { id: String },
28
29 #[error("Invalid PMID format: {pmid}")]
31 InvalidPmid { pmid: String },
32
33 #[error("Invalid PMC ID format: {pmcid}")]
35 InvalidPmcid { pmcid: String },
36
37 #[error("Invalid query: {0}")]
39 InvalidQuery(String),
40
41 #[error("API rate limit exceeded")]
43 RateLimitExceeded,
44
45 #[error("API error {status}: {message}")]
47 ApiError { status: u16, message: String },
48
49 #[error("IO error: {message}")]
51 IoError { message: String },
52
53 #[error("Search limit exceeded: requested {requested}, maximum is {maximum}")]
56 SearchLimitExceeded { requested: usize, maximum: usize },
57
58 #[error("History session expired or invalid: {0}")]
61 HistorySessionError(String),
62
63 #[error("WebEnv not available in search result")]
67 WebEnvNotAvailable,
68}
69
70pub type Result<T> = result::Result<T, PubMedError>;
71
72impl RetryableError for PubMedError {
73 fn is_retryable(&self) -> bool {
74 match self {
75 PubMedError::RequestError(err) => {
77 #[cfg(not(target_arch = "wasm32"))]
79 {
80 if err.is_timeout() || err.is_connect() {
81 return true;
82 }
83 }
84
85 #[cfg(target_arch = "wasm32")]
86 {
87 if err.is_timeout() {
88 return true;
89 }
90 }
91
92 if let Some(status) = err.status() {
94 return status.is_server_error() || status.as_u16() == 429;
95 }
96
97 !err.is_builder() && !err.is_redirect() && !err.is_decode()
99 }
100
101 PubMedError::RateLimitExceeded => true,
103
104 PubMedError::ApiError { status, message } => {
106 (*status >= 500 && *status < 600) || *status == 429 || {
108 let lower_msg = message.to_lowercase();
110 lower_msg.contains("temporarily unavailable")
111 || lower_msg.contains("timeout")
112 || lower_msg.contains("connection")
113 }
114 }
115
116 PubMedError::JsonError(_)
118 | PubMedError::XmlError(_)
119 | PubMedError::ArticleNotFound { .. }
120 | PubMedError::PmcNotAvailable { .. }
121 | PubMedError::InvalidPmid { .. }
122 | PubMedError::InvalidPmcid { .. }
123 | PubMedError::InvalidQuery(_)
124 | PubMedError::IoError { .. }
125 | PubMedError::SearchLimitExceeded { .. }
126 | PubMedError::HistorySessionError(_)
127 | PubMedError::WebEnvNotAvailable => false,
128 }
129 }
130
131 fn retry_reason(&self) -> &str {
132 if self.is_retryable() {
133 match self {
134 PubMedError::RequestError(err) if err.is_timeout() => "Request timeout",
135 #[cfg(not(target_arch = "wasm32"))]
136 PubMedError::RequestError(err) if err.is_connect() => "Connection error",
137 PubMedError::RequestError(_) => "Network error",
138 PubMedError::RateLimitExceeded => "Rate limit exceeded",
139 PubMedError::ApiError { status, .. } => match status {
140 429 => "Rate limit exceeded",
141 500..=599 => "Server error",
142 _ => "Temporary API error",
143 },
144 _ => "Transient error",
145 }
146 } else {
147 match self {
148 PubMedError::JsonError(_) => "Invalid JSON response",
149 PubMedError::XmlError(_) => "Invalid XML response",
150 PubMedError::ArticleNotFound { .. } => "Article does not exist",
151 PubMedError::PmcNotAvailable { .. } => "Content not available",
152 PubMedError::InvalidPmid { .. } | PubMedError::InvalidPmcid { .. } => {
153 "Invalid input"
154 }
155 PubMedError::InvalidQuery(_) => "Invalid query",
156 PubMedError::IoError { .. } => "File system error",
157 PubMedError::HistorySessionError(_) => "History session expired",
158 PubMedError::WebEnvNotAvailable => "WebEnv not available",
159 _ => "Non-transient error",
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
172 fn test_json_error_not_retryable() {
173 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
174 let err = PubMedError::JsonError(json_err);
175
176 assert!(!err.is_retryable());
177 assert_eq!(err.retry_reason(), "Invalid JSON response");
178 }
179
180 #[test]
181 fn test_xml_error_not_retryable() {
182 let err = PubMedError::XmlError("Invalid XML format".to_string());
183
184 assert!(!err.is_retryable());
185 assert_eq!(err.retry_reason(), "Invalid XML response");
186 }
187
188 #[test]
189 fn test_article_not_found_not_retryable() {
190 let err = PubMedError::ArticleNotFound {
191 pmid: "12345".to_string(),
192 };
193
194 assert!(!err.is_retryable());
195 assert_eq!(err.retry_reason(), "Article does not exist");
196 assert!(format!("{}", err).contains("12345"));
197 }
198
199 #[test]
200 fn test_pmc_not_available_not_retryable() {
201 let err = PubMedError::PmcNotAvailable {
202 id: "67890".to_string(),
203 };
204
205 assert!(!err.is_retryable());
206 assert_eq!(err.retry_reason(), "Content not available");
207 assert!(format!("{}", err).contains("67890"));
208 }
209
210 #[test]
211 fn test_pmc_not_available_by_pmcid_not_retryable() {
212 let err = PubMedError::PmcNotAvailable {
213 id: "PMC123456".to_string(),
214 };
215
216 assert!(!err.is_retryable());
217 assert_eq!(err.retry_reason(), "Content not available");
218 assert!(format!("{}", err).contains("PMC123456"));
219 }
220
221 #[test]
222 fn test_invalid_pmid_not_retryable() {
223 let err = PubMedError::InvalidPmid {
224 pmid: "invalid".to_string(),
225 };
226
227 assert!(!err.is_retryable());
228 assert_eq!(err.retry_reason(), "Invalid input");
229 assert!(format!("{}", err).contains("invalid"));
230 }
231
232 #[test]
233 fn test_invalid_pmcid_not_retryable() {
234 let err = PubMedError::InvalidPmcid {
235 pmcid: "PMCinvalid".to_string(),
236 };
237
238 assert!(!err.is_retryable());
239 assert_eq!(err.retry_reason(), "Invalid input");
240 assert!(format!("{}", err).contains("PMCinvalid"));
241 assert!(format!("{}", err).contains("Invalid PMC ID format"));
242 }
243
244 #[test]
245 fn test_invalid_query_not_retryable() {
246 let err = PubMedError::InvalidQuery("Empty query string".to_string());
247
248 assert!(!err.is_retryable());
249 assert_eq!(err.retry_reason(), "Invalid query");
250 assert!(format!("{}", err).contains("Empty query"));
251 }
252
253 #[test]
254 fn test_io_error_not_retryable() {
255 let err = PubMedError::IoError {
256 message: "File not found".to_string(),
257 };
258
259 assert!(!err.is_retryable());
260 assert_eq!(err.retry_reason(), "File system error");
261 assert!(format!("{}", err).contains("File not found"));
262 }
263
264 #[test]
265 fn test_search_limit_exceeded_not_retryable() {
266 let err = PubMedError::SearchLimitExceeded {
267 requested: 15000,
268 maximum: 10000,
269 };
270
271 assert!(!err.is_retryable());
272 assert!(format!("{}", err).contains("15000"));
273 assert!(format!("{}", err).contains("10000"));
274 }
275
276 #[test]
279 fn test_rate_limit_exceeded_is_retryable() {
280 let err = PubMedError::RateLimitExceeded;
281
282 assert!(err.is_retryable());
283 assert_eq!(err.retry_reason(), "Rate limit exceeded");
284 }
285
286 #[test]
287 fn test_api_error_429_is_retryable() {
288 let err = PubMedError::ApiError {
289 status: 429,
290 message: "Too Many Requests".to_string(),
291 };
292
293 assert!(err.is_retryable());
294 assert_eq!(err.retry_reason(), "Rate limit exceeded");
295 assert!(format!("{}", err).contains("429"));
296 }
297
298 #[test]
299 fn test_api_error_500_is_retryable() {
300 let err = PubMedError::ApiError {
301 status: 500,
302 message: "Internal Server Error".to_string(),
303 };
304
305 assert!(err.is_retryable());
306 assert_eq!(err.retry_reason(), "Server error");
307 }
308
309 #[test]
310 fn test_api_error_503_is_retryable() {
311 let err = PubMedError::ApiError {
312 status: 503,
313 message: "Service Unavailable".to_string(),
314 };
315
316 assert!(err.is_retryable());
317 assert_eq!(err.retry_reason(), "Server error");
318 }
319
320 #[test]
321 fn test_api_error_temporarily_unavailable_is_retryable() {
322 let err = PubMedError::ApiError {
323 status: 400,
324 message: "Service temporarily unavailable".to_string(),
325 };
326
327 assert!(err.is_retryable());
328 assert_eq!(err.retry_reason(), "Temporary API error");
329 }
330
331 #[test]
332 fn test_api_error_timeout_message_is_retryable() {
333 let err = PubMedError::ApiError {
334 status: 408,
335 message: "Request timeout".to_string(),
336 };
337
338 assert!(err.is_retryable());
339 assert_eq!(err.retry_reason(), "Temporary API error");
340 }
341
342 #[test]
343 fn test_api_error_connection_message_is_retryable() {
344 let err = PubMedError::ApiError {
345 status: 400,
346 message: "Connection reset by peer".to_string(),
347 };
348
349 assert!(err.is_retryable());
350 assert_eq!(err.retry_reason(), "Temporary API error");
351 }
352
353 #[test]
354 fn test_api_error_404_not_retryable() {
355 let err = PubMedError::ApiError {
356 status: 404,
357 message: "Not Found".to_string(),
358 };
359
360 assert!(!err.is_retryable());
361 }
362
363 #[test]
364 fn test_api_error_400_not_retryable() {
365 let err = PubMedError::ApiError {
366 status: 400,
367 message: "Bad Request".to_string(),
368 };
369
370 assert!(!err.is_retryable());
371 }
372
373 #[test]
376 fn test_error_display_messages() {
377 let test_cases = vec![
378 (
379 PubMedError::XmlError("test".to_string()),
380 "XML parsing failed: test",
381 ),
382 (
383 PubMedError::InvalidQuery("bad query".to_string()),
384 "Invalid query: bad query",
385 ),
386 (PubMedError::RateLimitExceeded, "API rate limit exceeded"),
387 ];
388
389 for (error, expected_message) in test_cases {
390 assert_eq!(format!("{}", error), expected_message);
391 }
392 }
393
394 #[test]
395 fn test_error_display_with_fields() {
396 let err = PubMedError::ArticleNotFound {
397 pmid: "12345".to_string(),
398 };
399 let display = format!("{}", err);
400 assert!(display.contains("Article not found"));
401 assert!(display.contains("12345"));
402
403 let err = PubMedError::ApiError {
404 status: 500,
405 message: "Server Error".to_string(),
406 };
407 let display = format!("{}", err);
408 assert!(display.contains("500"));
409 assert!(display.contains("Server Error"));
410 }
411
412 #[test]
413 fn test_result_type_alias() {
414 fn returns_ok() -> Result<String> {
416 Ok("success".to_string())
417 }
418
419 fn returns_err() -> Result<String> {
420 Err(PubMedError::RateLimitExceeded)
421 }
422
423 assert!(returns_ok().is_ok());
424 assert!(returns_err().is_err());
425 }
426}