Skip to main content

everymap_core/error/
mod.rs

1use thiserror::Error;
2
3/// Unified error type for all EveryMap operations.
4///
5/// Provides structured error information across HTTP, authentication,
6/// provider-specific, rate limiting, and validation scenarios.
7#[derive(Error, Debug)]
8pub enum EveryMapError {
9    /// HTTP request failed with a status code.
10    #[error("HTTP error {status}: {message}")]
11    HttpError {
12        status: u16,
13        message: String,
14        body: Option<String>,
15    },
16
17    /// Authentication failed (invalid API key, expired token, etc.).
18    #[error("Authentication failed: {message}")]
19    AuthError { message: String, provider: String },
20
21    /// Provider-specific error (e.g., HERE returned an error response).
22    #[error("{provider} error: {code} - {message}")]
23    ProviderError {
24        provider: String,
25        code: String,
26        message: String,
27    },
28
29    /// Rate limited by the provider.
30    #[error("Rate limited by {provider}, retry after {retry_after_secs:?}s")]
31    RateLimited {
32        provider: String,
33        retry_after_secs: Option<u64>,
34    },
35
36    /// Request validation failed (invalid coordinates, missing required fields, etc.).
37    #[error("Validation error: {0}")]
38    ValidationError(String),
39
40    /// Failed to deserialize a response body.
41    #[error("Failed to deserialize response: {source}")]
42    SerializationError { source: serde_json::Error },
43
44    /// An error occurred in the underlying HTTP client.
45    #[error("HTTP client error: {0}")]
46    ClientError(#[from] reqwest::Error),
47
48    /// The requested domain is not supported by this provider.
49    #[error("{provider} does not support {domain}")]
50    UnsupportedDomain { provider: String, domain: String },
51
52    /// An unknown or unexpected error occurred.
53    #[error("Unknown error occurred")]
54    Unknown,
55}
56
57impl EveryMapError {
58    /// Create an HTTP error from a status code and message.
59    pub fn http(status: u16, message: impl Into<String>) -> Self {
60        Self::HttpError {
61            status,
62            message: message.into(),
63            body: None,
64        }
65    }
66
67    /// Create an HTTP error with a response body.
68    pub fn http_with_body(
69        status: u16,
70        message: impl Into<String>,
71        body: impl Into<String>,
72    ) -> Self {
73        Self::HttpError {
74            status,
75            message: message.into(),
76            body: Some(body.into()),
77        }
78    }
79
80    /// Create a provider error.
81    pub fn provider(
82        provider: impl Into<String>,
83        code: impl Into<String>,
84        message: impl Into<String>,
85    ) -> Self {
86        Self::ProviderError {
87            provider: provider.into(),
88            code: code.into(),
89            message: message.into(),
90        }
91    }
92
93    /// Create an auth error.
94    pub fn auth(provider: impl Into<String>, message: impl Into<String>) -> Self {
95        Self::AuthError {
96            provider: provider.into(),
97            message: message.into(),
98        }
99    }
100
101    /// Create a rate limit error.
102    pub fn rate_limited(provider: impl Into<String>, retry_after_secs: Option<u64>) -> Self {
103        Self::RateLimited {
104            provider: provider.into(),
105            retry_after_secs,
106        }
107    }
108
109    /// Check if this is an HTTP error with the given status code.
110    pub fn is_status(&self, status: u16) -> bool {
111        match self {
112            Self::HttpError { status: s, .. } => *s == status,
113            _ => false,
114        }
115    }
116
117    /// Check if this is a rate limit error (HTTP 429).
118    pub fn is_rate_limited(&self) -> bool {
119        matches!(self, Self::RateLimited { .. }) || self.is_status(429)
120    }
121
122    /// Check if this is an authentication error (HTTP 401 or 403).
123    pub fn is_auth_error(&self) -> bool {
124        matches!(self, Self::AuthError { .. }) || self.is_status(401) || self.is_status(403)
125    }
126
127    /// Create an unsupported domain error.
128    pub fn unsupported_domain(provider: impl Into<String>, domain: impl Into<String>) -> Self {
129        Self::UnsupportedDomain {
130            provider: provider.into(),
131            domain: domain.into(),
132        }
133    }
134}
135
136// Maintain backward compatibility: `From<reqwest::Error>` now maps to `ClientError`
137// rather than `HttpError` since we have richer HTTP error handling now.
138// `SerializationError` keeps its `from` impl for serde_json::Error.
139
140impl From<serde_json::Error> for EveryMapError {
141    fn from(err: serde_json::Error) -> Self {
142        Self::SerializationError { source: err }
143    }
144}
145
146pub type EveryMapResult<T> = Result<T, EveryMapError>;
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_http_error_construction() {
154        let err = EveryMapError::http(404, "Not Found");
155        assert!(err.is_status(404));
156        assert!(!err.is_status(200));
157        assert!(!err.is_rate_limited());
158    }
159
160    #[test]
161    fn test_http_error_with_body() {
162        let err = EveryMapError::http_with_body(500, "Internal Error", "details");
163        assert!(err.is_status(500));
164    }
165
166    #[test]
167    fn test_auth_error() {
168        let err = EveryMapError::auth("here", "Invalid API key");
169        assert!(err.is_auth_error());
170    }
171
172    #[test]
173    fn test_rate_limited_error() {
174        let err = EveryMapError::rate_limited("here", Some(60));
175        assert!(err.is_rate_limited());
176    }
177
178    #[test]
179    fn test_unsupported_domain_error() {
180        let err = EveryMapError::unsupported_domain("google", "traffic");
181        match &err {
182            EveryMapError::UnsupportedDomain { provider, domain } => {
183                assert_eq!(provider, "google");
184                assert_eq!(domain, "traffic");
185            }
186            _ => panic!("Expected UnsupportedDomain error"),
187        }
188        assert!(!err.is_auth_error());
189        assert!(!err.is_rate_limited());
190    }
191
192    #[test]
193    fn test_provider_error() {
194        let err = EveryMapError::provider("here", "E400", "Bad request");
195        match &err {
196            EveryMapError::ProviderError {
197                provider,
198                code,
199                message,
200            } => {
201                assert_eq!(provider, "here");
202                assert_eq!(code, "E400");
203                assert_eq!(message, "Bad request");
204            }
205            _ => panic!("Expected ProviderError"),
206        }
207    }
208
209    #[test]
210    fn test_status_401_is_auth_error() {
211        let err = EveryMapError::http(401, "Unauthorized");
212        assert!(err.is_auth_error());
213    }
214
215    #[test]
216    fn test_status_429_is_rate_limited() {
217        let err = EveryMapError::http(429, "Too Many Requests");
218        assert!(err.is_rate_limited());
219    }
220
221    // --- All error variants construction ---
222
223    #[test]
224    fn test_http_error_construction_basic() {
225        let err = EveryMapError::http(500, "Server Error");
226        assert!(err.is_status(500));
227        assert!(!err.is_status(200));
228    }
229
230    #[test]
231    fn test_http_error_with_body_construction() {
232        let err = EveryMapError::http_with_body(502, "Bad Gateway", "upstream timeout");
233        assert!(err.is_status(502));
234    }
235
236    #[test]
237    fn test_auth_error_construction() {
238        let err = EveryMapError::auth("google", "API key expired");
239        assert!(err.is_auth_error());
240        assert!(!err.is_rate_limited());
241    }
242
243    #[test]
244    fn test_provider_error_construction() {
245        let err = EveryMapError::provider("tomtom", "E404", "Not found");
246        assert!(!err.is_auth_error());
247        assert!(!err.is_rate_limited());
248    }
249
250    #[test]
251    fn test_rate_limited_construction() {
252        let err = EveryMapError::rate_limited("here", Some(30));
253        assert!(err.is_rate_limited());
254        assert!(!err.is_auth_error());
255    }
256
257    #[test]
258    fn test_rate_limited_without_retry_after() {
259        let err = EveryMapError::rate_limited("mapbox", None);
260        assert!(err.is_rate_limited());
261    }
262
263    #[test]
264    fn test_validation_error_construction() {
265        let err = EveryMapError::ValidationError("Coordinates out of range".to_string());
266        assert!(!err.is_auth_error());
267        assert!(!err.is_rate_limited());
268    }
269
270    #[test]
271    fn test_serialization_error_construction() {
272        let json_str = "not valid json{{{";
273        let serde_err: Result<serde_json::Value, _> = serde_json::from_str(json_str);
274        let err = EveryMapError::from(serde_err.unwrap_err());
275        assert!(!err.is_auth_error());
276        assert!(!err.is_rate_limited());
277    }
278
279    #[test]
280    fn test_unsupported_domain_construction() {
281        let err = EveryMapError::unsupported_domain("radar", "imaging");
282        assert!(!err.is_auth_error());
283        assert!(!err.is_rate_limited());
284    }
285
286    #[test]
287    fn test_unknown_error_construction() {
288        let err = EveryMapError::Unknown;
289        assert!(!err.is_auth_error());
290        assert!(!err.is_rate_limited());
291    }
292
293    // --- Display formatting for each variant ---
294
295    #[test]
296    fn test_display_http_error() {
297        let err = EveryMapError::http(404, "Not Found");
298        let message = format!("{}", err);
299        assert!(message.contains("404"));
300        assert!(message.contains("Not Found"));
301    }
302
303    #[test]
304    fn test_display_auth_error() {
305        let err = EveryMapError::auth("here", "Invalid key");
306        let message = format!("{}", err);
307        assert!(message.contains("Authentication failed"));
308        assert!(message.contains("Invalid key"));
309    }
310
311    #[test]
312    fn test_display_provider_error() {
313        let err = EveryMapError::provider("here", "E400", "Bad request");
314        let message = format!("{}", err);
315        assert!(message.contains("here"));
316        assert!(message.contains("E400"));
317        assert!(message.contains("Bad request"));
318    }
319
320    #[test]
321    fn test_display_rate_limited() {
322        let err = EveryMapError::rate_limited("google", Some(60));
323        let message = format!("{}", err);
324        assert!(message.contains("Rate limited"));
325        assert!(message.contains("google"));
326        assert!(message.contains("60"));
327    }
328
329    #[test]
330    fn test_display_rate_limited_no_retry() {
331        let err = EveryMapError::rate_limited("google", None);
332        let message = format!("{}", err);
333        assert!(message.contains("Rate limited"));
334    }
335
336    #[test]
337    fn test_display_validation_error() {
338        let err = EveryMapError::ValidationError("bad input".to_string());
339        let message = format!("{}", err);
340        assert!(message.contains("Validation error"));
341        assert!(message.contains("bad input"));
342    }
343
344    #[test]
345    fn test_display_serialization_error() {
346        let serde_err = serde_json::from_str::<serde_json::Value>("{bad}").unwrap_err();
347        let err = EveryMapError::from(serde_err);
348        let message = format!("{}", err);
349        assert!(message.contains("Failed to deserialize"));
350    }
351
352    #[test]
353    fn test_display_unsupported_domain() {
354        let err = EveryMapError::unsupported_domain("google", "traffic");
355        let message = format!("{}", err);
356        assert!(message.contains("google"));
357        assert!(message.contains("traffic"));
358    }
359
360    #[test]
361    fn test_display_unknown() {
362        let err = EveryMapError::Unknown;
363        let message = format!("{}", err);
364        assert!(message.contains("Unknown"));
365    }
366
367    // --- is_status with various codes ---
368
369    #[test]
370    fn test_is_status_200() {
371        let err = EveryMapError::http(200, "OK");
372        assert!(err.is_status(200));
373        assert!(!err.is_status(201));
374    }
375
376    #[test]
377    fn test_is_status_403() {
378        let err = EveryMapError::http(403, "Forbidden");
379        assert!(err.is_status(403));
380    }
381
382    #[test]
383    fn test_is_status_non_http_error_returns_false() {
384        let err = EveryMapError::ValidationError("test".to_string());
385        assert!(!err.is_status(400));
386    }
387
388    // --- is_rate_limited with RateLimited and HttpError 429 ---
389
390    #[test]
391    fn test_is_rate_limited_rate_limited_variant() {
392        let err = EveryMapError::rate_limited("here", Some(60));
393        assert!(err.is_rate_limited());
394    }
395
396    #[test]
397    fn test_is_rate_limited_http_429() {
398        let err = EveryMapError::http(429, "Too Many Requests");
399        assert!(err.is_rate_limited());
400    }
401
402    #[test]
403    fn test_is_rate_limited_http_other_code() {
404        let err = EveryMapError::http(500, "Server Error");
405        assert!(!err.is_rate_limited());
406    }
407
408    // --- is_auth_error with AuthError and HttpError 401/403 ---
409
410    #[test]
411    fn test_is_auth_error_auth_variant() {
412        let err = EveryMapError::auth("here", "Invalid key");
413        assert!(err.is_auth_error());
414    }
415
416    #[test]
417    fn test_is_auth_error_http_401() {
418        let err = EveryMapError::http(401, "Unauthorized");
419        assert!(err.is_auth_error());
420    }
421
422    #[test]
423    fn test_is_auth_error_http_403() {
424        let err = EveryMapError::http(403, "Forbidden");
425        assert!(err.is_auth_error());
426    }
427
428    #[test]
429    fn test_is_auth_error_http_other_code() {
430        let err = EveryMapError::http(500, "Server Error");
431        assert!(!err.is_auth_error());
432    }
433
434    // --- From<serde_json::Error> conversion ---
435
436    #[test]
437    fn test_from_serde_json_error() {
438        let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
439        let err: EveryMapError = serde_err.into();
440        match err {
441            EveryMapError::SerializationError { .. } => {}
442            _ => panic!("Expected SerializationError"),
443        }
444    }
445
446    #[test]
447    fn test_from_serde_json_error_is_not_auth() {
448        let serde_err = serde_json::from_str::<serde_json::Value>("{").unwrap_err();
449        let err: EveryMapError = serde_err.into();
450        assert!(!err.is_auth_error());
451        assert!(!err.is_rate_limited());
452    }
453
454    // --- HttpError body field ---
455
456    #[test]
457    fn test_http_error_with_body_contains_body() {
458        match EveryMapError::http_with_body(500, "Error", "detailed body") {
459            EveryMapError::HttpError {
460                status,
461                message,
462                body,
463            } => {
464                assert_eq!(status, 500);
465                assert_eq!(message, "Error");
466                assert_eq!(body, Some("detailed body".to_string()));
467            }
468            _ => panic!("Expected HttpError"),
469        }
470    }
471
472    #[test]
473    fn test_http_error_without_body() {
474        match EveryMapError::http(404, "Not Found") {
475            EveryMapError::HttpError {
476                status,
477                message,
478                body,
479            } => {
480                assert_eq!(status, 404);
481                assert_eq!(message, "Not Found");
482                assert_eq!(body, None);
483            }
484            _ => panic!("Expected HttpError"),
485        }
486    }
487
488    // --- RateLimited retry_after_secs field ---
489
490    #[test]
491    fn test_rate_limited_retry_after_some() {
492        match EveryMapError::rate_limited("here", Some(120)) {
493            EveryMapError::RateLimited {
494                provider,
495                retry_after_secs,
496            } => {
497                assert_eq!(provider, "here");
498                assert_eq!(retry_after_secs, Some(120));
499            }
500            _ => panic!("Expected RateLimited"),
501        }
502    }
503
504    #[test]
505    fn test_rate_limited_retry_after_none() {
506        match EveryMapError::rate_limited("here", None) {
507            EveryMapError::RateLimited {
508                retry_after_secs, ..
509            } => {
510                assert_eq!(retry_after_secs, None);
511            }
512            _ => panic!("Expected RateLimited"),
513        }
514    }
515}