modio/
error.rs

1//! Client errors
2use std::fmt;
3use std::time::Duration;
4
5use http::StatusCode;
6
7use crate::types::Error as ApiError;
8
9/// A `Result` alias where the `Err` case is `modio::Error`.
10pub type Result<T, E = Error> = std::result::Result<T, E>;
11
12/// The Errors that may occur when using `Modio`.
13pub struct Error {
14    inner: Box<Inner>,
15}
16
17type BoxError = Box<dyn std::error::Error + Send + Sync>;
18
19struct Inner {
20    kind: Kind,
21    error_ref: Option<u16>,
22    source: Option<BoxError>,
23}
24
25impl Error {
26    #[inline]
27    pub(crate) fn new(kind: Kind) -> Self {
28        Self {
29            inner: Box::new(Inner {
30                kind,
31                error_ref: None,
32                source: None,
33            }),
34        }
35    }
36
37    #[inline]
38    pub(crate) fn with<E: Into<BoxError>>(mut self, source: E) -> Self {
39        self.inner.source = Some(source.into());
40        self
41    }
42
43    #[inline]
44    pub(crate) fn with_error_ref(mut self, error_ref: u16) -> Self {
45        self.inner.error_ref = Some(error_ref);
46        self
47    }
48
49    /// Returns true if the API key/access token is incorrect, revoked, expired or the request
50    /// needs a different authentication method.
51    pub fn is_auth(&self) -> bool {
52        matches!(self.inner.kind, Kind::Unauthorized | Kind::TokenRequired)
53    }
54
55    /// Returns true if the acceptance of the Terms of Use is required before continuing external
56    /// authorization.
57    pub fn is_terms_acceptance_required(&self) -> bool {
58        matches!(self.inner.kind, Kind::TermsAcceptanceRequired)
59    }
60
61    /// Returns true if the error is from a type Builder.
62    pub fn is_builder(&self) -> bool {
63        matches!(self.inner.kind, Kind::Builder)
64    }
65
66    /// Returns true if the rate limit associated with credentials has been exhausted.
67    pub fn is_ratelimited(&self) -> bool {
68        matches!(self.inner.kind, Kind::RateLimit { .. })
69    }
70
71    /// Returns true if the error was generated from a response.
72    pub fn is_response(&self) -> bool {
73        matches!(self.inner.kind, Kind::Response { .. })
74    }
75
76    /// Returns true if the error contains validation errors.
77    pub fn is_validation(&self) -> bool {
78        matches!(self.inner.kind, Kind::Validation { .. })
79    }
80
81    /// Returns the API error if the error was generated from a response.
82    pub fn api_error(&self) -> Option<&ApiError> {
83        match &self.inner.kind {
84            Kind::Response { error, .. } => Some(error),
85            _ => None,
86        }
87    }
88
89    /// Returns modio's error reference code.
90    ///
91    /// See the [Error Codes](https://docs.mod.io/restapiref/#error-codes) docs for more information.
92    pub fn error_ref(&self) -> Option<u16> {
93        self.inner.error_ref
94    }
95
96    /// Returns status code if the error was generated from a response.
97    pub fn status(&self) -> Option<StatusCode> {
98        match self.inner.kind {
99            Kind::Response { status, .. } => Some(status),
100            _ => None,
101        }
102    }
103
104    /// Returns validation message & errors from the response.
105    pub fn validation(&self) -> Option<(&String, &Vec<(String, String)>)> {
106        match self.inner.kind {
107            Kind::Validation {
108                ref message,
109                ref errors,
110            } => Some((message, errors)),
111            _ => None,
112        }
113    }
114}
115
116impl fmt::Debug for Error {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        let mut builder = f.debug_struct("modio::Error");
119
120        builder.field("kind", &self.inner.kind);
121        if let Some(ref error_ref) = self.inner.error_ref {
122            builder.field("error_ref", error_ref);
123        }
124
125        if let Some(ref source) = self.inner.source {
126            builder.field("source", source);
127        }
128        builder.finish()
129    }
130}
131
132impl fmt::Display for Error {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self.inner.kind {
135            Kind::Unauthorized => f.write_str("unauthorized")?,
136            Kind::TokenRequired => f.write_str("access token is required")?,
137            Kind::TermsAcceptanceRequired => f.write_str("terms acceptance is required")?,
138            Kind::Builder => f.write_str("builder error")?,
139            Kind::Request => f.write_str("http request error")?,
140            Kind::Service => f.write_str("service request error")?,
141            Kind::Response { status, .. } => {
142                let prefix = if status.is_client_error() {
143                    "HTTP status client error"
144                } else {
145                    debug_assert!(status.is_server_error());
146                    "HTTP status server error"
147                };
148                write!(f, "{prefix} ({status})")?;
149            }
150            Kind::RateLimit { retry_after } => {
151                write!(f, "API rate limit reached. Try again in {retry_after:?}.")?;
152            }
153            Kind::Validation {
154                ref message,
155                ref errors,
156            } => {
157                write!(f, "validation failed: '{message}' {errors:?}")?;
158            }
159        }
160        if let Some(ref e) = self.inner.source {
161            write!(f, ": {e}")?;
162        }
163        Ok(())
164    }
165}
166
167impl std::error::Error for Error {
168    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
169        self.inner.source.as_ref().map(|e| &**e as _)
170    }
171}
172
173#[derive(Debug)]
174pub(crate) enum Kind {
175    /// API key/access token is incorrect, revoked or expired.
176    Unauthorized,
177    /// Access token is required to perform the action.
178    TokenRequired,
179    /// The acceptance of the Terms of Use is required.
180    TermsAcceptanceRequired,
181    Validation {
182        message: String,
183        errors: Vec<(String, String)>,
184    },
185    RateLimit {
186        retry_after: Duration,
187    },
188    Builder,
189    Request,
190    Service,
191    Response {
192        status: StatusCode,
193        error: ApiError,
194    },
195}
196
197pub(crate) fn token_required() -> Error {
198    Error::new(Kind::TokenRequired)
199}
200
201pub(crate) fn builder<E: Into<BoxError>>(source: E) -> Error {
202    Error::new(Kind::Builder).with(source)
203}
204
205pub(crate) fn request<E: Into<BoxError>>(source: E) -> Error {
206    Error::new(Kind::Request).with(source)
207}
208
209pub(crate) fn service<E: Into<BoxError>>(source: E) -> Error {
210    Error::new(Kind::Service).with(source)
211}
212
213pub(crate) fn error_for_status(status: StatusCode, error: ApiError) -> Error {
214    let error_ref = error.error_ref;
215    let kind = match status {
216        StatusCode::UNPROCESSABLE_ENTITY => Kind::Validation {
217            message: error.message,
218            errors: error.errors,
219        },
220        StatusCode::UNAUTHORIZED => Kind::Unauthorized,
221        StatusCode::FORBIDDEN if error_ref == 11051 => Kind::TermsAcceptanceRequired,
222        _ => Kind::Response { status, error },
223    };
224    Error::new(kind).with_error_ref(error_ref)
225}
226
227pub(crate) fn ratelimit(retry_after: u64) -> Error {
228    Error::new(Kind::RateLimit {
229        retry_after: Duration::from_secs(retry_after),
230    })
231}