1use crate::models::ErrorResponse;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, DocarooError>;
8
9#[derive(Error, Debug)]
11pub enum DocarooError {
12 #[error("HTTP request failed: {0}")]
14 RequestFailed(#[from] reqwest::Error),
15
16 #[error("API error: {message} (code: {code})")]
18 ApiError {
19 code: String,
21 message: String,
23 request_id: Option<String>,
25 },
26
27 #[error("Invalid request: {0}")]
29 InvalidRequest(String),
30
31 #[error("Rate limit exceeded. Retry after {retry_after} seconds")]
33 RateLimitExceeded {
34 retry_after: u64,
36 },
37
38 #[error("Authentication failed: {0}")]
40 AuthenticationFailed(String),
41
42 #[error("Failed to parse response: {0}")]
44 ParseError(String),
45
46 #[error("Invalid URL: {0}")]
48 UrlError(#[from] url::ParseError),
49}
50
51impl DocarooError {
52 pub fn from_error_response(response: ErrorResponse) -> Self {
54 match response.error.as_str() {
55 "rate_limit_exceeded" => {
56 let retry_after = response
57 .details
58 .as_ref()
59 .and_then(|d| d.get("retryAfter"))
60 .and_then(|v| v.as_u64())
61 .unwrap_or(60);
62 Self::RateLimitExceeded { retry_after }
63 }
64 "unauthorized" => Self::AuthenticationFailed(response.message),
65 _ => Self::ApiError {
66 code: response.error,
67 message: response.message,
68 request_id: response.request_id,
69 },
70 }
71 }
72
73 pub fn is_retryable(&self) -> bool {
75 matches!(
76 self,
77 Self::RequestFailed(_) | Self::RateLimitExceeded { .. }
78 )
79 }
80
81 pub fn request_id(&self) -> Option<&str> {
83 match self {
84 Self::ApiError { request_id, .. } => request_id.as_deref(),
85 _ => None,
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use chrono::Utc;
94
95 #[test]
96 fn test_error_from_response() {
97 let error_response = ErrorResponse {
98 error: "bad_request".to_string(),
99 message: "Invalid NPI format".to_string(),
100 details: None,
101 request_id: Some("req_123".to_string()),
102 timestamp: Some(Utc::now()),
103 };
104
105 let error = DocarooError::from_error_response(error_response);
106 match error {
107 DocarooError::ApiError {
108 code,
109 message,
110 request_id,
111 } => {
112 assert_eq!(code, "bad_request");
113 assert_eq!(message, "Invalid NPI format");
114 assert_eq!(request_id, Some("req_123".to_string()));
115 }
116 _ => panic!("Expected ApiError"),
117 }
118 }
119
120 #[test]
121 fn test_rate_limit_error() {
122 let error_response = ErrorResponse {
123 error: "rate_limit_exceeded".to_string(),
124 message: "Too many requests".to_string(),
125 details: Some(serde_json::json!({ "retryAfter": 120 })),
126 request_id: None,
127 timestamp: None,
128 };
129
130 let error = DocarooError::from_error_response(error_response);
131 match error {
132 DocarooError::RateLimitExceeded { retry_after } => {
133 assert_eq!(retry_after, 120);
134 }
135 _ => panic!("Expected RateLimitExceeded"),
136 }
137 }
138
139 #[test]
140 fn test_is_retryable() {
141 let rate_limit_error = DocarooError::RateLimitExceeded { retry_after: 60 };
142 assert!(rate_limit_error.is_retryable());
143
144 let api_error = DocarooError::ApiError {
145 code: "bad_request".to_string(),
146 message: "Invalid request".to_string(),
147 request_id: None,
148 };
149 assert!(!api_error.is_retryable());
150 }
151}