brawl_api/
error.rs

1//! Contains the `Error` enum, used in all functions of the library that may error (fetches).
2
3use std::result::Result as StdResult;
4use std::error::Error as StdError;
5
6use serde::{self, Serialize, Deserialize};
7use serde_json::{self, Error as SerdeError, Value as JsonValue};
8use url::ParseError as UrlError;
9use reqwest::blocking::Response;
10use reqwest::{
11    Error as ReqwestError, StatusCode, Response as AResponse,
12    header::{InvalidHeaderValue, HeaderMap}
13};
14use std::fmt::{Formatter, Display};
15use crate::util::JsonMap;
16
17
18/// Represents a `brawl-api` Result type.
19pub type Result<T> = StdResult<T, Error>;
20
21/// Represents all possible errors while using methods from this lib (`brawl-api`) in functions
22/// such as fetches.
23#[non_exhaustive]
24#[derive(Debug)]
25pub enum Error {
26    /// Represents an error occurred while using `serde_json` for serializing/deserializing JSON
27    /// data. (A `serde_json` crate error)
28    Json(SerdeError),
29
30    /// Represents an error indicating a malformed URL.
31    Url(UrlError),
32
33    /// Represents an error indicating that an invalid Authorization header was specified.
34    /// This error can be caused by the user of this lib when an invalid auth key is given.
35    /// (Note that invalid auth key is covered by the Status error, but if it is given in an
36    /// invalid format - which is impossible to be a valid key -, this error may appear.)
37    /// Contains the invalid header value inside (as the `.0` field).
38    Authorization(InvalidHeaderValue),
39
40    /// Represents an error occurred while requesting to the API or while receiving its data.
41    /// (A `reqwest` crate error)
42    Request(ReqwestError),
43
44    /// Represents an API ratelimit.
45    Ratelimited {
46        /// Maximum amount of requests per minute allowed. None indicates this was not given.
47        limit: Option<usize>,
48
49        /// Amount remaining (this should normally be 0). None indicates this was not given
50        remaining: Option<usize>,
51
52        /// Stringified timestamp (seconds) at which the ratelimit block will be lifted, or None
53        /// for not ratelimited. This is only an Option in case a change is needed, considering
54        /// that this will always be a `Some(String)` if this specific error is raised.
55        time_until_reset: Option<String>,
56    },
57
58//    /// Represents a JSON decoding error, with a description and the offending value.
59//    Decode(&'static str, JsonValue),  // Could have use in the future if the api adds POST
60
61    /// Represents an arbitrary status code error received from the API.
62    /// E.g. 400, 403, 404, 429, 500, 503
63    ///
64    /// - Field `.0` is the status code object;
65    /// - Field `.1` is an optional instance of [`APIError`], if it may be parsed like so;
66    /// - Field `.2` is the raw error response as parsed json, if it had that format.
67    /// (If field .2 is None, that implies field .1 is None, since an APIError comes from a JSON
68    /// object).
69    ///
70    /// [`APIError`]: struct.APIError.html
71    Status(StatusCode, Option<APIError>, Option<JsonValue>),
72
73    /// Represents an error while operating the conversion of types through [`FetchFrom`]. Note that
74    /// any errors while *fetching* things are either an `Error::Json` or `Error::Request`, while
75    /// this error refers to additional operations done *after* the fetching is done.
76    ///
77    /// At field `.0`, there is a `String` object describing what occurred.
78    ///
79    /// [`FetchFrom`]: ../traits/trait.FetchFrom.html
80    FetchFrom(String),
81
82    /// Represents an error while using [`TimeLike.parse`]. Note that this is feature-gated
83    /// by the `chrono` feature (meaning that, if it is disabled, this variant is removed).
84    ///
85    /// [`TimeLike.parse`]: ../time/struct.TimeLike.html#method.parse
86    #[cfg(feature = "chrono")]
87    ParseTimeLike {
88        /// The reason why this error occurred.
89        reason: String,
90
91        /// The offending (invalid) string that triggered this error, if any.
92        offender: Option<String>,
93
94        /// The original [`chrono::ParseError`], if there was any (otherwise, it's a custom
95        /// error triggered by the library).
96        ///
97        /// [`chrono::ParseError`]: https://docs.rs/chrono/*/chrono/format/struct.ParseError.html
98        original_err: Option<::chrono::ParseError>,
99    },
100}
101
102/// Represents an error given by the API, with its specifications.
103#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
104pub struct APIError {
105    /// The reason for the error.
106    #[serde(default)]
107    pub reason: String,
108
109    /// Optionally, a human-readable message for the error.
110    #[serde(default)]
111    pub message: Option<String>,
112
113    /// Optionally, a specific type of this error.
114    #[serde(default)]
115    #[serde(rename = "type")]
116    pub err_type: Option<String>,
117
118    /// Optionally, any extra details about this error.
119    #[serde(default)]
120    pub detail: Option<JsonMap>,
121}
122
123impl Default for APIError {
124    fn default() -> APIError {
125        APIError {
126            reason: String::from(""),
127            message: None,
128            err_type: None,
129            detail: None,
130        }
131    }
132}
133
134impl From<SerdeError> for Error {
135    fn from(err: SerdeError) -> Error {
136        Error::Json(err)
137    }
138}
139
140impl From<ReqwestError> for Error {
141    fn from(err: ReqwestError) -> Error {
142        Error::Request(err)
143    }
144}
145
146impl From<UrlError> for Error {
147    fn from(err: UrlError) -> Error {
148        Error::Url(err)
149    }
150}
151
152#[cfg(feature = "chrono")]
153impl From<::chrono::ParseError> for Error {
154    fn from(err: ::chrono::ParseError) -> Error {
155        Error::ParseTimeLike {
156            reason: err.to_string(),
157            offender: None,
158            original_err: Some(err),
159        }
160    }
161}
162
163
164impl Display for Error {
165    fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
166        match *self {
167            Error::Json(ref e) => e.fmt(f),
168            Error::Request(ref e) => e.fmt(f),
169            _ => f.write_str(&*self.description()),
170            // _ => f.write_str(self.description())
171        }
172    }
173}
174
175
176
177impl StdError for Error {
178
179    fn source(&self) -> Option<&(dyn StdError + 'static)> {
180        match *self {
181            Error::Json(ref e) => Some(e),
182            Error::Url(ref e) => Some(e),
183            Error::Request(ref e) => Some(e),
184
185            #[cfg(feature = "chrono")]
186            Error::ParseTimeLike { ref original_err, .. } => match original_err {
187                Some(ref e) => Some(e),
188                None => None,
189            },
190
191            _ => None,
192        }
193    }
194}
195
196impl Error {
197
198    fn description(&self) -> String {
199        match *self {
200            Error::Json(ref e) => String::from(e.description()),
201
202            Error::Authorization(_) => String::from(
203                "Auth key was provided in an invalid format for a header."
204            ),
205
206            Error::Url(_) => String::from("Invalid URL was given/built."),
207
208            Error::Ratelimited { limit, ref time_until_reset, .. } => {
209                let lim_part = match limit {
210                    Some(lim) => format!(" Limit of {} requests/min.", lim),
211                    None => String::from(""),
212                };
213
214                let time_part = match *time_until_reset {
215                    Some(ref timeur) => format!(" Resets at timestamp {}.", timeur),
216                    None => String::from(""),
217                };
218
219                let dot = {
220                    if limit.is_none() && time_until_reset.is_none() {
221                        "."
222                    } else {
223                        ":"
224                    }
225                };
226
227                format!("Ratelimited{}{}{}", dot, lim_part, time_part)
228            },
229
230            Error::Request(ref e) => String::from(e.description()),
231
232//            Error::Decode(msg, _) => String::from(msg),
233
234            Error::Status(ref status, _, _) => String::from(
235                status.canonical_reason().unwrap_or(
236                    "Unknown HTTP status code error received"
237                )
238            ),
239
240            Error::FetchFrom(ref string) => string.clone(),
241
242            #[cfg(feature = "chrono")]
243            Error::ParseTimeLike {
244                ref reason,
245                ..
246            } => reason.clone(),
247        }
248    }
249
250    /// Obtain an Error from a Response (blocking). Optionally specify a pre-parsed JsonValue
251    /// for the body, otherwise that parsing will be done inside this function.
252    #[doc(hidden)]
253    pub(crate) fn from_response(response: Response, value: Option<JsonValue>) -> Error {
254        let status = response.status();
255
256        let headers: &HeaderMap = response.headers();
257        let headers = headers.clone();
258
259        let value: Option<JsonValue> = match value {
260            Some(val) => Some(val),
261            None => serde_json::from_reader(response).ok()
262        };
263
264        let reset_header = headers.get("x-ratelimit-reset");
265        if let Some(reset_header) = reset_header {  // ratelimited
266            let reset_header = reset_header.to_str();
267            if let Ok(reset) = reset_header {
268                return Error::Ratelimited {
269                    limit: match headers.get("x-ratelimit-limit") {
270                        Some(lim_header) => lim_header.to_str().ok().and_then(
271                            |s| { s.parse().ok() }
272                        ),
273                        None => None,
274                    },
275
276                    remaining: match headers.get("x-ratelimit-remaining") {
277                        Some(rem_header) => rem_header.to_str().ok().and_then(
278                            |s| { s.parse().ok() }
279                        ),
280                        None => None,
281                    },
282
283                    time_until_reset: Some(String::from(reset)),
284                }
285            }
286        }
287
288        let api_error: Option<APIError> = match value {
289            Some(ref val) => serde_json::from_value(val.clone()).ok(),
290            None => None,
291        };
292
293        Error::Status(status, api_error, value)
294    }
295
296    /// Obtain an Error from a Response (non-blocking). Optionally specify a pre-parsed JsonValue
297    /// for the body, otherwise that parsing will be done inside this function.
298    #[doc(hidden)]
299    #[cfg(feature = "async")]
300    pub(crate) async fn a_from_response(response: AResponse, value: Option<JsonValue>) -> Error {
301        let status = response.status();
302        let headers: &HeaderMap = response.headers();
303        let headers = headers.clone();
304
305        let value: Option<JsonValue> = match value {
306            Some(val) => Some(val),
307            None => response.json().await.ok()
308        };
309
310        let reset_header = headers.get("x-ratelimit-reset");
311        if let Some(reset_header) = reset_header {  // ratelimited
312            let reset_header = reset_header.to_str();
313            if let Ok(reset) = reset_header {
314                return Error::Ratelimited {
315                    limit: match headers.get("x-ratelimit-limit") {
316                        Some(lim_header) => lim_header.to_str().ok().and_then(
317                            |s| { s.parse().ok() }
318                        ),
319                        None => None,
320                    },
321
322                    remaining: match headers.get("x-ratelimit-remaining") {
323                        Some(rem_header) => rem_header.to_str().ok().and_then(
324                            |s| { s.parse().ok() }
325                        ),
326                        None => None,
327                    },
328
329                    time_until_reset: Some(String::from(reset)),
330                }
331            }
332        }
333
334        let api_error: Option<APIError> = match value {
335            Some(ref val) => serde_json::from_value(val.clone()).ok(),
336            None => None,
337        };
338
339        Error::Status(status, api_error, value)
340    }
341
342
343}