darkstrata_credential_check/
errors.rs1use std::time::Duration;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum DarkStrataError {
9 #[error("Authentication failed: {message}")]
11 Authentication {
12 message: String,
14 status_code: Option<u16>,
16 },
17
18 #[error("Validation error: {message}")]
20 Validation {
21 message: String,
23 field: Option<String>,
25 },
26
27 #[error("API error: {message}")]
29 Api {
30 message: String,
32 status_code: Option<u16>,
34 retryable: bool,
36 },
37
38 #[error("Request timed out after {duration:?}")]
40 Timeout {
41 duration: Duration,
43 },
44
45 #[error("Network error: {message}")]
47 Network {
48 message: String,
50 #[source]
52 source: Option<Box<dyn std::error::Error + Send + Sync>>,
53 },
54
55 #[error("Rate limit exceeded")]
57 RateLimit {
58 retry_after: Option<Duration>,
60 },
61}
62
63impl DarkStrataError {
64 pub fn authentication(message: impl Into<String>) -> Self {
66 Self::Authentication {
67 message: message.into(),
68 status_code: Some(401),
69 }
70 }
71
72 pub fn validation(message: impl Into<String>) -> Self {
74 Self::Validation {
75 message: message.into(),
76 field: None,
77 }
78 }
79
80 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 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 pub fn timeout(duration: Duration) -> Self {
100 Self::Timeout { duration }
101 }
102
103 pub fn network(message: impl Into<String>) -> Self {
105 Self::Network {
106 message: message.into(),
107 source: None,
108 }
109 }
110
111 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 pub fn rate_limit(retry_after: Option<Duration>) -> Self {
124 Self::RateLimit { retry_after }
125 }
126
127 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 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 pub fn retry_after(&self) -> Option<Duration> {
153 match self {
154 Self::RateLimit { retry_after } => *retry_after,
155 _ => None,
156 }
157 }
158}
159
160pub type Result<T> = std::result::Result<T, DarkStrataError>;
162
163pub fn is_retryable_status(status: u16) -> bool {
165 matches!(status, 408 | 429 | 500 | 502 | 503 | 504)
166}
167
168impl From<reqwest::Error> for DarkStrataError {
170 fn from(err: reqwest::Error) -> Self {
171 if err.is_timeout() {
172 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}