Skip to main content

darkstrata_credential_check/
errors.rs

1//! Error types for the DarkStrata Credential Check SDK.
2
3use std::time::Duration;
4use thiserror::Error;
5
6/// Main error type for the DarkStrata SDK.
7#[derive(Debug, Error)]
8pub enum DarkStrataError {
9    /// Authentication failed (invalid or missing API key).
10    #[error("Authentication failed: {message}")]
11    Authentication {
12        /// Error message.
13        message: String,
14        /// HTTP status code (typically 401).
15        status_code: Option<u16>,
16    },
17
18    /// Input validation failed.
19    #[error("Validation error: {message}")]
20    Validation {
21        /// Error message describing the validation failure.
22        message: String,
23        /// The field that failed validation (if applicable).
24        field: Option<String>,
25    },
26
27    /// API request failed.
28    #[error("API error: {message}")]
29    Api {
30        /// Error message.
31        message: String,
32        /// HTTP status code.
33        status_code: Option<u16>,
34        /// Whether this error is retryable.
35        retryable: bool,
36    },
37
38    /// Request timed out.
39    #[error("Request timed out after {duration:?}")]
40    Timeout {
41        /// The timeout duration that was exceeded.
42        duration: Duration,
43    },
44
45    /// Network connectivity error.
46    #[error("Network error: {message}")]
47    Network {
48        /// Error message.
49        message: String,
50        /// The underlying error.
51        #[source]
52        source: Option<Box<dyn std::error::Error + Send + Sync>>,
53    },
54
55    /// Rate limit exceeded.
56    #[error("Rate limit exceeded")]
57    RateLimit {
58        /// Duration to wait before retrying (from Retry-After header).
59        retry_after: Option<Duration>,
60    },
61}
62
63impl DarkStrataError {
64    /// Create an authentication error.
65    pub fn authentication(message: impl Into<String>) -> Self {
66        Self::Authentication {
67            message: message.into(),
68            status_code: Some(401),
69        }
70    }
71
72    /// Create a validation error.
73    pub fn validation(message: impl Into<String>) -> Self {
74        Self::Validation {
75            message: message.into(),
76            field: None,
77        }
78    }
79
80    /// Create a validation error for a specific field.
81    pub fn validation_field(field: impl Into<String>, message: impl Into<String>) -> Self {
82        Self::Validation {
83            message: message.into(),
84            field: Some(field.into()),
85        }
86    }
87
88    /// Create an API error.
89    pub fn api(message: impl Into<String>, status_code: Option<u16>) -> Self {
90        let retryable = status_code.is_some_and(is_retryable_status);
91        Self::Api {
92            message: message.into(),
93            status_code,
94            retryable,
95        }
96    }
97
98    /// Create a timeout error.
99    pub fn timeout(duration: Duration) -> Self {
100        Self::Timeout { duration }
101    }
102
103    /// Create a network error.
104    pub fn network(message: impl Into<String>) -> Self {
105        Self::Network {
106            message: message.into(),
107            source: None,
108        }
109    }
110
111    /// Create a network error with a source.
112    pub fn network_with_source(
113        message: impl Into<String>,
114        source: impl std::error::Error + Send + Sync + 'static,
115    ) -> Self {
116        Self::Network {
117            message: message.into(),
118            source: Some(Box::new(source)),
119        }
120    }
121
122    /// Create a rate limit error.
123    pub fn rate_limit(retry_after: Option<Duration>) -> Self {
124        Self::RateLimit { retry_after }
125    }
126
127    /// Check if this error is retryable.
128    pub fn is_retryable(&self) -> bool {
129        match self {
130            Self::Authentication { .. } => false,
131            Self::Validation { .. } => false,
132            Self::Api { retryable, .. } => *retryable,
133            Self::Timeout { .. } => true,
134            Self::Network { .. } => true,
135            Self::RateLimit { .. } => true,
136        }
137    }
138
139    /// Get the HTTP status code if available.
140    pub fn status_code(&self) -> Option<u16> {
141        match self {
142            Self::Authentication { status_code, .. } => *status_code,
143            Self::Validation { .. } => None,
144            Self::Api { status_code, .. } => *status_code,
145            Self::Timeout { .. } => None,
146            Self::Network { .. } => None,
147            Self::RateLimit { .. } => Some(429),
148        }
149    }
150
151    /// Get the retry-after duration for rate limit errors.
152    pub fn retry_after(&self) -> Option<Duration> {
153        match self {
154            Self::RateLimit { retry_after } => *retry_after,
155            _ => None,
156        }
157    }
158}
159
160/// Result type alias for DarkStrata operations.
161pub type Result<T> = std::result::Result<T, DarkStrataError>;
162
163/// Check if an HTTP status code indicates a retryable error.
164pub fn is_retryable_status(status: u16) -> bool {
165    matches!(status, 408 | 429 | 500 | 502 | 503 | 504)
166}
167
168/// Convert a reqwest error to a DarkStrataError.
169impl From<reqwest::Error> for DarkStrataError {
170    fn from(err: reqwest::Error) -> Self {
171        if err.is_timeout() {
172            // Default timeout - actual duration not available from reqwest error
173            Self::Timeout {
174                duration: Duration::from_secs(30),
175            }
176        } else if err.is_connect() {
177            Self::Network {
178                message: format!("Connection failed: {}", err),
179                source: Some(Box::new(err)),
180            }
181        } else if let Some(status) = err.status() {
182            let status_code = status.as_u16();
183            if status_code == 401 {
184                Self::Authentication {
185                    message: "Invalid or missing API key".to_string(),
186                    status_code: Some(status_code),
187                }
188            } else if status_code == 429 {
189                Self::RateLimit { retry_after: None }
190            } else {
191                Self::Api {
192                    message: format!("Request failed: {}", err),
193                    status_code: Some(status_code),
194                    retryable: is_retryable_status(status_code),
195                }
196            }
197        } else {
198            Self::Network {
199                message: format!("Request failed: {}", err),
200                source: Some(Box::new(err)),
201            }
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_authentication_error() {
212        let err = DarkStrataError::authentication("Invalid API key");
213        assert!(!err.is_retryable());
214        assert_eq!(err.status_code(), Some(401));
215        assert!(err.to_string().contains("Invalid API key"));
216    }
217
218    #[test]
219    fn test_validation_error() {
220        let err = DarkStrataError::validation("Email is required");
221        assert!(!err.is_retryable());
222        assert_eq!(err.status_code(), None);
223    }
224
225    #[test]
226    fn test_validation_field_error() {
227        let err = DarkStrataError::validation_field("email", "Email is required");
228        assert!(!err.is_retryable());
229        if let DarkStrataError::Validation { field, .. } = err {
230            assert_eq!(field, Some("email".to_string()));
231        } else {
232            panic!("Expected Validation error");
233        }
234    }
235
236    #[test]
237    fn test_api_error_retryable() {
238        let err = DarkStrataError::api("Server error", Some(503));
239        assert!(err.is_retryable());
240        assert_eq!(err.status_code(), Some(503));
241    }
242
243    #[test]
244    fn test_api_error_not_retryable() {
245        let err = DarkStrataError::api("Bad request", Some(400));
246        assert!(!err.is_retryable());
247        assert_eq!(err.status_code(), Some(400));
248    }
249
250    #[test]
251    fn test_timeout_error() {
252        let err = DarkStrataError::timeout(Duration::from_secs(30));
253        assert!(err.is_retryable());
254        assert_eq!(err.status_code(), None);
255    }
256
257    #[test]
258    fn test_network_error() {
259        let err = DarkStrataError::network("Connection refused");
260        assert!(err.is_retryable());
261        assert_eq!(err.status_code(), None);
262    }
263
264    #[test]
265    fn test_rate_limit_error() {
266        let err = DarkStrataError::rate_limit(Some(Duration::from_secs(60)));
267        assert!(err.is_retryable());
268        assert_eq!(err.status_code(), Some(429));
269        assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
270    }
271
272    #[test]
273    fn test_is_retryable_status() {
274        assert!(!is_retryable_status(400));
275        assert!(!is_retryable_status(401));
276        assert!(!is_retryable_status(403));
277        assert!(!is_retryable_status(404));
278        assert!(is_retryable_status(408));
279        assert!(is_retryable_status(429));
280        assert!(is_retryable_status(500));
281        assert!(is_retryable_status(502));
282        assert!(is_retryable_status(503));
283        assert!(is_retryable_status(504));
284    }
285}