Skip to main content

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}