docaroo_rs/
error.rs

1//! Error handling for the Docaroo API client
2
3use crate::models::ErrorResponse;
4use thiserror::Error;
5
6/// Result type alias for Docaroo operations
7pub type Result<T> = std::result::Result<T, DocarooError>;
8
9/// Errors that can occur when interacting with the Docaroo API
10#[derive(Error, Debug)]
11pub enum DocarooError {
12    /// HTTP request failed
13    #[error("HTTP request failed: {0}")]
14    RequestFailed(#[from] reqwest::Error),
15
16    /// API returned an error response
17    #[error("API error: {message} (code: {code})")]
18    ApiError {
19        /// Error code from the API
20        code: String,
21        /// Error message from the API
22        message: String,
23        /// Optional request ID for support
24        request_id: Option<String>,
25    },
26
27    /// Invalid request parameters
28    #[error("Invalid request: {0}")]
29    InvalidRequest(String),
30
31    /// Rate limit exceeded
32    #[error("Rate limit exceeded. Retry after {retry_after} seconds")]
33    RateLimitExceeded {
34        /// Number of seconds to wait before retrying
35        retry_after: u64,
36    },
37
38    /// Authentication failed
39    #[error("Authentication failed: {0}")]
40    AuthenticationFailed(String),
41
42    /// Deserialization error
43    #[error("Failed to parse response: {0}")]
44    ParseError(String),
45
46    /// URL parsing error
47    #[error("Invalid URL: {0}")]
48    UrlError(#[from] url::ParseError),
49}
50
51impl DocarooError {
52    /// Create an API error from an error response
53    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    /// Check if this error is retryable
74    pub fn is_retryable(&self) -> bool {
75        matches!(
76            self,
77            Self::RequestFailed(_) | Self::RateLimitExceeded { .. }
78        )
79    }
80
81    /// Get the request ID if available (for support purposes)
82    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}