openfigi_rs/error.rs
1//! Error handling types for OpenFIGI API operations.
2//!
3//! This module provides comprehensive error types and utilities for handling failures
4//! that can occur during OpenFIGI API interactions. The main error type [`crate::error::OpenFIGIError`]
5//! unifies different error sources into a single, easy-to-handle enum.
6//!
7//! ## Error Categories
8//!
9//! [`crate::error::OpenFIGIError`] covers all possible failure scenarios:
10//!
11//! - **Network errors**: Connection failures, timeouts, DNS resolution issues
12//! - **HTTP errors**: Status codes (400, 401, 404, 429, 500, etc.) with detailed context
13//! - **Parsing errors**: JSON deserialization failures and malformed responses
14//! - **Middleware errors**: Retry policy exhaustion, request building failures
15//! - **URL errors**: Invalid URL formation and parsing issues
16//! - **IO errors**: File system operations (for caching, logging, etc.)
17//!
18//! ## Error Inspection
19//!
20//! Use convenient inspection methods to categorize errors without pattern matching:
21//!
22//! ```rust
23//! use openfigi_rs::error::OpenFIGIError;
24//!
25//! fn handle_error(err: OpenFIGIError) {
26//! if err.is_status() {
27//! if let Some(status) = err.status() {
28//! eprintln!("HTTP error: {status}");
29//! }
30//! } else if err.is_timeout() {
31//! eprintln!("Request timed out - consider retry");
32//! } else if err.is_connect() {
33//! eprintln!("Connection failed - check network");
34//! }
35//! }
36//! ```
37//!
38//! ## Error Conversion
39//!
40//! Common error types are automatically converted via `From` implementations:
41//!
42//! ```compile_fail
43//! use openfigi_rs::error::OpenFIGIError;
44//!
45//! // These conversions happen automatically
46//! let reqwest_err: reqwest::Error = /* ... */;
47//! let openfigi_err: OpenFIGIError = reqwest_err.into();
48//!
49//! let json_err: serde_json::Error = /* ... */;
50//! let openfigi_err: OpenFIGIError = json_err.into();
51//! ```
52
53use std::{error, fmt};
54use url::Url;
55
56/// Type alias for `Result<T, OpenFIGIError>`.
57///
58/// Convenience type used throughout the crate for consistent error handling.
59pub type Result<T> = std::result::Result<T, OpenFIGIError>;
60
61/// Main error type for all OpenFIGI API operations.
62///
63/// This enum unifies all possible error types that can occur during OpenFIGI API
64/// interactions, providing a single error type for consistent handling across
65/// the entire crate. Each variant wraps a specific error type while maintaining
66/// the original error information.
67///
68/// ## Design Philosophy
69///
70/// Rather than requiring consumers to handle multiple error types, `OpenFIGIError`
71/// provides a unified interface with convenient inspection methods. This allows
72/// for both simple error handling (treat all errors the same) and sophisticated
73/// error handling (inspect specific error types).
74///
75/// ## Inspection Methods
76///
77/// The error type provides numerous `is_*()` methods to check error categories
78/// without pattern matching on variants. This makes error handling more ergonomic
79/// and future-proof as new error variants can be added without breaking existing code.
80///
81/// # Examples
82///
83/// ```rust
84/// use openfigi_rs::error::OpenFIGIError;
85///
86/// async fn handle_request_error(err: OpenFIGIError) {
87/// match err.status() {
88/// Some(status) if status.is_client_error() => {
89/// eprintln!("Client error {}: check request parameters", status);
90/// }
91/// Some(status) if status.is_server_error() => {
92/// eprintln!("Server error {}: retry may help", status);
93/// }
94/// None if err.is_timeout() => {
95/// eprintln!("Request timeout: retry with backoff");
96/// }
97/// None if err.is_connect() => {
98/// eprintln!("Connection error: check network connectivity");
99/// }
100/// _ => {
101/// eprintln!("Other error: {}", err);
102/// }
103/// }
104/// }
105/// ```
106#[derive(Debug)]
107pub enum OpenFIGIError {
108 /// HTTP client error from the underlying reqwest library.
109 ///
110 /// Includes network issues, timeout errors, connection failures,
111 /// and other HTTP-level problems.
112 ReqwestError(reqwest::Error),
113
114 /// Middleware stack error from reqwest-middleware.
115 ///
116 /// Occurs when middleware components (retry policies, logging, etc.)
117 /// fail or when the middleware stack itself encounters issues.
118 ReqwestMiddlewareError(reqwest_middleware::Error),
119
120 /// URL parsing error when constructing request URLs.
121 ///
122 /// Typically indicates malformed base URLs or invalid URL components.
123 UrlParseError(url::ParseError),
124
125 /// JSON serialization or deserialization error.
126 ///
127 /// Occurs when request payloads cannot be serialized or when
128 /// response bodies cannot be parsed as valid JSON.
129 SerdeError(serde_json::Error),
130
131 /// File system I/O error for operations like caching or logging.
132 ///
133 /// May occur during file-based operations if implemented in the future.
134 IoError(std::io::Error),
135
136 /// HTTP response error with detailed status and content information.
137 ///
138 /// Contains structured error information from the OpenFIGI API,
139 /// including status codes and response body content.
140 ResponseError(ResponseContent),
141
142 /// Miscellaneous application-specific errors.
143 ///
144 /// Used for validation errors and other issues that don't fit
145 /// into the other categories.
146 OtherError {
147 /// Error classification
148 kind: OtherErrorKind,
149 /// Error description
150 message: String,
151 },
152}
153
154/// HTTP response error details.
155///
156/// Contains status code, optional message, and response body content
157/// for detailed error analysis and debugging.
158#[derive(Debug, Clone)]
159pub struct ResponseContent {
160 /// HTTP status code
161 pub status: reqwest::StatusCode,
162 /// Additional error context message
163 pub message: String,
164 /// Raw response body content
165 pub content: String,
166}
167
168/// Classification for miscellaneous errors that don't fit other categories.
169///
170/// This enum provides additional categorization for application-specific
171/// errors that aren't covered by the main error variants.
172#[derive(Debug, Clone, PartialEq)]
173#[non_exhaustive]
174pub enum OtherErrorKind {
175 /// Request validation errors.
176 ///
177 /// Indicates that request parameters failed validation before
178 /// being sent to the API.
179 Validation,
180 /// Unexpected API response errors.
181 ///
182 /// Indicates that the API returned an unexpected response format,
183 /// such as multiple results when only one was expected.
184 UnexpectedApiResponse,
185 /// Unclassified errors.
186 ///
187 /// Catch-all category for errors that don't fit other classifications.
188 Other,
189}
190
191impl fmt::Display for OpenFIGIError {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 // Most common errors first for better branch prediction
195 Self::ReqwestError(e) => write!(f, "error in reqwest: {e}"),
196 Self::ResponseError(e) => match (e.message.is_empty(), e.content.is_empty()) {
197 (false, false) => write!(
198 f,
199 "error in response: status code {}: {} | content: {}",
200 e.status, e.message, e.content
201 ),
202 (false, true) => write!(
203 f,
204 "error in response: status code {}: {}",
205 e.status, e.message
206 ),
207 (true, false) => write!(
208 f,
209 "error in response: status code {} | content: {}",
210 e.status, e.content
211 ),
212 (true, true) => write!(f, "error in response: status code {}", e.status),
213 },
214 Self::SerdeError(e) => write!(f, "error in serde: {e}"),
215 Self::ReqwestMiddlewareError(e) => {
216 write!(f, "error in reqwest-middleware: {e}")
217 }
218 Self::UrlParseError(e) => write!(f, "error in url: {e}"),
219 Self::IoError(e) => write!(f, "error in IO: {e}"),
220 Self::OtherError { kind, message } => {
221 write!(f, "error in other: {kind:?}: {message}")
222 }
223 }
224 }
225}
226
227impl error::Error for OpenFIGIError {
228 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
229 match self {
230 Self::ReqwestError(e) => Some(e),
231 Self::ReqwestMiddlewareError(e) => Some(e),
232 Self::SerdeError(e) => Some(e),
233 Self::IoError(e) => Some(e),
234 Self::UrlParseError(e) => Some(e),
235 _ => None,
236 }
237 }
238}
239
240impl From<reqwest::Error> for OpenFIGIError {
241 fn from(e: reqwest::Error) -> Self {
242 Self::ReqwestError(e)
243 }
244}
245
246impl From<reqwest_middleware::Error> for OpenFIGIError {
247 fn from(e: reqwest_middleware::Error) -> Self {
248 Self::ReqwestMiddlewareError(e)
249 }
250}
251
252impl From<url::ParseError> for OpenFIGIError {
253 fn from(e: url::ParseError) -> Self {
254 Self::UrlParseError(e)
255 }
256}
257
258impl From<serde_json::Error> for OpenFIGIError {
259 fn from(e: serde_json::Error) -> Self {
260 Self::SerdeError(e)
261 }
262}
263
264impl From<std::io::Error> for OpenFIGIError {
265 fn from(e: std::io::Error) -> Self {
266 Self::IoError(e)
267 }
268}
269
270impl OpenFIGIError {
271 /// Returns the URL associated with this error, if available.
272 ///
273 /// Provides access to the request URL for errors that occurred during
274 /// HTTP operations. Useful for debugging and logging.
275 ///
276 /// # Examples
277 ///
278 /// ```rust
279 /// use openfigi_rs::error::OpenFIGIError;
280 ///
281 /// fn log_error_with_url(err: &OpenFIGIError) {
282 /// if let Some(url) = err.url() {
283 /// eprintln!("Error occurred for URL: {}", url);
284 /// }
285 /// }
286 /// ```
287 #[must_use]
288 pub fn url(&self) -> Option<&Url> {
289 match self {
290 Self::ReqwestError(inner) => inner.url(),
291 Self::ReqwestMiddlewareError(inner) => inner.url(),
292 _ => None,
293 }
294 }
295
296 /// Returns a mutable reference to the URL for this error.
297 ///
298 /// Useful for removing sensitive information from URLs before logging
299 /// or displaying errors to users.
300 ///
301 /// # Examples
302 ///
303 /// ```rust
304 /// use openfigi_rs::error::OpenFIGIError;
305 ///
306 /// fn sanitize_error_url(mut err: OpenFIGIError) -> OpenFIGIError {
307 /// if let Some(url) = err.url_mut() {
308 /// url.set_query(None); // Remove query parameters
309 /// }
310 /// err
311 /// }
312 /// ```
313 #[must_use]
314 pub fn url_mut(&mut self) -> Option<&mut Url> {
315 match self {
316 Self::ReqwestError(inner) => inner.url_mut(),
317 Self::ReqwestMiddlewareError(inner) => inner.url_mut(),
318 _ => None,
319 }
320 }
321
322 /// Returns a new error with the specified URL attached.
323 ///
324 /// Attaches URL information to errors that support it. Only applies
325 /// to reqwest and middleware errors; other error types are returned unchanged.
326 #[must_use]
327 pub fn with_url(self, url: Url) -> Self {
328 match self {
329 Self::ReqwestError(inner) => Self::ReqwestError(inner.with_url(url)),
330 Self::ReqwestMiddlewareError(inner) => {
331 Self::ReqwestMiddlewareError(inner.with_url(url))
332 }
333 // Not applicable for other variants
334 _ => self,
335 }
336 }
337
338 /// Returns an error with the URL removed for security purposes.
339 ///
340 /// Removes URL information from errors that contain it. Useful when
341 /// URLs might contain sensitive information that shouldn't be logged.
342 #[must_use]
343 pub fn without_url(self) -> Self {
344 match self {
345 Self::ReqwestError(inner) => Self::ReqwestError(inner.without_url()),
346 Self::ReqwestMiddlewareError(inner) => {
347 Self::ReqwestMiddlewareError(inner.without_url())
348 }
349 // Not applicable for other variants
350 _ => self,
351 }
352 }
353
354 /// Returns true if this error originated from middleware.
355 ///
356 /// Identifies errors that occurred within the middleware stack,
357 /// such as retry policy exhaustion or middleware-specific failures.
358 #[must_use]
359 pub fn is_middleware(&self) -> bool {
360 match self {
361 Self::ReqwestMiddlewareError(inner) => inner.is_middleware(),
362 // Not applicable for other variants
363 _ => false,
364 }
365 }
366
367 /// Returns true if this error originated from the builder methods.
368 #[must_use]
369 pub fn is_builder(&self) -> bool {
370 match self {
371 Self::ReqwestError(inner) => inner.is_builder(),
372 Self::ReqwestMiddlewareError(inner) => inner.is_builder(),
373 // Not applicable for other variants
374 _ => false,
375 }
376 }
377
378 /// Returns true if this error is a redirect error.
379 ///
380 /// Identifies errors related to HTTP redirects, such as too many redirects
381 /// or redirect loops.
382 #[must_use]
383 pub fn is_redirect(&self) -> bool {
384 match self {
385 Self::ReqwestError(inner) => inner.is_redirect(),
386 Self::ReqwestMiddlewareError(inner) => inner.is_redirect(),
387 // Not applicable for other variants
388 _ => false,
389 }
390 }
391
392 /// Returns true if this error is a status error.
393 ///
394 /// Indicates errors that contain HTTP status codes, either from reqwest
395 /// or from explicit response errors.
396 #[must_use]
397 pub fn is_status(&self) -> bool {
398 match self {
399 Self::ReqwestError(inner) => inner.is_status(),
400 Self::ReqwestMiddlewareError(inner) => inner.is_status(),
401 Self::ResponseError(_) => true,
402 // Not applicable for other variants
403 _ => false,
404 }
405 }
406
407 /// Returns true if this error is a timeout error.
408 ///
409 /// Indicates that the HTTP request exceeded the configured timeout period.
410 /// This can help distinguish between connection issues and slow responses.
411 #[must_use]
412 pub fn is_timeout(&self) -> bool {
413 match self {
414 Self::ReqwestError(inner) => inner.is_timeout(),
415 Self::ReqwestMiddlewareError(inner) => inner.is_timeout(),
416 // Not applicable for other variants
417 _ => false,
418 }
419 }
420
421 /// Returns true if this error is a request error.
422 ///
423 /// Indicates errors that occurred during request processing,
424 /// such as malformed request data or invalid parameters.
425 #[must_use]
426 pub fn is_request(&self) -> bool {
427 match self {
428 Self::ReqwestError(inner) => inner.is_request(),
429 Self::ReqwestMiddlewareError(inner) => inner.is_request(),
430 // Not applicable for other variants
431 _ => false,
432 }
433 }
434
435 /// Returns true if this error is a connection error.
436 ///
437 /// Indicates network-level connection failures, such as DNS resolution
438 /// problems, connection refused, or network unreachable errors.
439 #[must_use]
440 pub fn is_connect(&self) -> bool {
441 match self {
442 Self::ReqwestError(inner) => inner.is_connect(),
443 Self::ReqwestMiddlewareError(inner) => inner.is_connect(),
444 // Not applicable for other variants
445 _ => false,
446 }
447 }
448
449 /// Returns true if this error is related to the request or response body.
450 ///
451 /// Identifies errors that occurred during body processing, such as
452 /// reading response bodies or serializing request payloads.
453 #[must_use]
454 pub fn is_body(&self) -> bool {
455 match self {
456 Self::ReqwestError(inner) => inner.is_body(),
457 Self::ReqwestMiddlewareError(inner) => inner.is_body(),
458 // Not applicable for other variants
459 _ => false,
460 }
461 }
462
463 /// Returns true if this error is a decode error.
464 ///
465 /// Indicates errors that occurred during response deserialization or
466 /// other data decoding operations. Includes JSON parsing failures
467 /// and format conversion errors.
468 #[must_use]
469 pub fn is_decode(&self) -> bool {
470 match self {
471 Self::ReqwestError(inner) => inner.is_decode(),
472 Self::ReqwestMiddlewareError(inner) => inner.is_decode(),
473 Self::OtherError { .. } => true,
474 // Not applicable for other variants
475 _ => false,
476 }
477 }
478
479 /// Returns the HTTP status code associated with this error, if available.
480 ///
481 /// Extracts the HTTP status code from errors that contain one, such as
482 /// reqwest errors with status information or explicit response errors.
483 /// Returns `None` for errors that don't have an associated status code.
484 ///
485 /// # Examples
486 ///
487 /// ```rust
488 /// use openfigi_rs::error::OpenFIGIError;
489 ///
490 /// fn handle_status_error(err: &OpenFIGIError) {
491 /// if let Some(status) = err.status() {
492 /// match status.as_u16() {
493 /// 400 => eprintln!("Bad request - check parameters"),
494 /// 401 => eprintln!("Unauthorized - check API key"),
495 /// 429 => eprintln!("Rate limited - retry later"),
496 /// 500..=599 => eprintln!("Server error - retry may help"),
497 /// _ => eprintln!("HTTP error: {}", status),
498 /// }
499 /// }
500 /// }
501 /// ```
502 #[must_use]
503 pub fn status(&self) -> Option<reqwest::StatusCode> {
504 match self {
505 Self::ReqwestError(inner) => inner.status(),
506 Self::ReqwestMiddlewareError(inner) => inner.status(),
507 Self::ResponseError(resp) => Some(resp.status),
508 // Not applicable for other variants
509 _ => None,
510 }
511 }
512
513 #[doc(hidden)]
514 /// Creates a new `ResponseError` with the given parameters.
515 ///
516 /// This is an internal constructor used by the client to create response errors
517 /// with structured information about HTTP failures.
518 ///
519 /// # Arguments
520 ///
521 /// * `status` - HTTP status code from the response
522 /// * `content` - Raw response body content
523 /// * `message` - Optional additional error context message
524 pub(crate) fn response_error(
525 status: reqwest::StatusCode,
526 message: impl Into<String>,
527 content: impl Into<String>,
528 ) -> Self {
529 Self::ResponseError(ResponseContent {
530 status,
531 message: message.into(),
532 content: content.into(),
533 })
534 }
535
536 #[doc(hidden)]
537 /// Creates a new `OtherError` with the given kind and message.
538 ///
539 /// This is an internal constructor for application-specific errors that
540 /// don't fit into the other error categories.
541 ///
542 /// # Arguments
543 ///
544 /// * `kind` - Classification of the error type
545 /// * `message` - Descriptive error message
546 pub(crate) fn other_error(kind: OtherErrorKind, message: impl Into<String>) -> Self {
547 Self::OtherError {
548 kind,
549 message: message.into(),
550 }
551 }
552}