1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
//! Contains the `Error` enum, used in all functions of the library that may error (fetches).

use std::result::Result as StdResult;
use std::error::Error as StdError;

use serde::{self, Serialize, Deserialize};
use serde_json::{self, Error as SerdeError, Value as JsonValue};
use url::ParseError as UrlError;
use reqwest::blocking::Response;
use reqwest::{
    Error as ReqwestError, StatusCode, Response as AResponse,
    header::{InvalidHeaderValue, HeaderMap}
};
use std::fmt::{Formatter, Display};
use crate::util::JsonMap;


/// Represents a `brawl-api` Result type.
pub type Result<T> = StdResult<T, Error>;

/// Represents all possible errors while using methods from this lib (`brawl-api`) in functions
/// such as fetches.
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
    /// Represents an error occurred while using `serde_json` for serializing/deserializing JSON
    /// data. (A `serde_json` crate error)
    Json(SerdeError),

    /// Represents an error indicating a malformed URL.
    Url(UrlError),

    /// Represents an error indicating that an invalid Authorization header was specified.
    /// This error can be caused by the user of this lib when an invalid auth key is given.
    /// (Note that invalid auth key is covered by the Status error, but if it is given in an
    /// invalid format - which is impossible to be a valid key -, this error may appear.)
    /// Contains the invalid header value inside (as the `.0` field).
    Authorization(InvalidHeaderValue),

    /// Represents an error occurred while requesting to the API or while receiving its data.
    /// (A `reqwest` crate error)
    Request(ReqwestError),

    /// Represents an API ratelimit.
    Ratelimited {
        /// Maximum amount of requests per minute allowed. None indicates this was not given.
        limit: Option<usize>,

        /// Amount remaining (this should normally be 0). None indicates this was not given
        remaining: Option<usize>,

        /// Stringified timestamp (seconds) at which the ratelimit block will be lifted, or None
        /// for not ratelimited. This is only an Option in case a change is needed, considering
        /// that this will always be a `Some(String)` if this specific error is raised.
        time_until_reset: Option<String>,
    },

//    /// Represents a JSON decoding error, with a description and the offending value.
//    Decode(&'static str, JsonValue),  // Could have use in the future if the api adds POST

    /// Represents an arbitrary status code error received from the API.
    /// E.g. 400, 403, 404, 429, 500, 503
    ///
    /// - Field `.0` is the status code object;
    /// - Field `.1` is an optional instance of [`APIError`], if it may be parsed like so;
    /// - Field `.2` is the raw error response as parsed json, if it had that format.
    /// (If field .2 is None, that implies field .1 is None, since an APIError comes from a JSON
    /// object).
    ///
    /// [`APIError`]: struct.APIError.html
    Status(StatusCode, Option<APIError>, Option<JsonValue>),

    /// Represents an error while operating the conversion of types through [`FetchFrom`]. Note that
    /// any errors while *fetching* things are either an `Error::Json` or `Error::Request`, while
    /// this error refers to additional operations done *after* the fetching is done.
    ///
    /// At field `.0`, there is a `String` object describing what occurred.
    ///
    /// [`FetchFrom`]: ../traits/trait.FetchFrom.html
    FetchFrom(String),

    /// Represents an error while using [`TimeLike.parse`]. Note that this is feature-gated
    /// by the `chrono` feature (meaning that, if it is disabled, this variant is removed).
    ///
    /// [`TimeLike.parse`]: ../time/struct.TimeLike.html#method.parse
    #[cfg(feature = "chrono")]
    ParseTimeLike {
        /// The reason why this error occurred.
        reason: String,

        /// The offending (invalid) string that triggered this error, if any.
        offender: Option<String>,

        /// The original [`chrono::ParseError`], if there was any (otherwise, it's a custom
        /// error triggered by the library).
        ///
        /// [`chrono::ParseError`]: https://docs.rs/chrono/*/chrono/format/struct.ParseError.html
        original_err: Option<::chrono::ParseError>,
    },
}

/// Represents an error given by the API, with its specifications.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct APIError {
    /// The reason for the error.
    #[serde(default)]
    pub reason: String,

    /// Optionally, a human-readable message for the error.
    #[serde(default)]
    pub message: Option<String>,

    /// Optionally, a specific type of this error.
    #[serde(default)]
    #[serde(rename = "type")]
    pub err_type: Option<String>,

    /// Optionally, any extra details about this error.
    #[serde(default)]
    pub detail: Option<JsonMap>,
}

impl Default for APIError {
    fn default() -> APIError {
        APIError {
            reason: String::from(""),
            message: None,
            err_type: None,
            detail: None,
        }
    }
}

impl From<SerdeError> for Error {
    fn from(err: SerdeError) -> Error {
        Error::Json(err)
    }
}

impl From<ReqwestError> for Error {
    fn from(err: ReqwestError) -> Error {
        Error::Request(err)
    }
}

impl From<UrlError> for Error {
    fn from(err: UrlError) -> Error {
        Error::Url(err)
    }
}

#[cfg(feature = "chrono")]
impl From<::chrono::ParseError> for Error {
    fn from(err: ::chrono::ParseError) -> Error {
        Error::ParseTimeLike {
            reason: err.to_string(),
            offender: None,
            original_err: Some(err),
        }
    }
}


impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
        match *self {
            Error::Json(ref e) => e.fmt(f),
            Error::Request(ref e) => e.fmt(f),
            _ => f.write_str(&*self.description()),
            // _ => f.write_str(self.description())
        }
    }
}



impl StdError for Error {

    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        match *self {
            Error::Json(ref e) => Some(e),
            Error::Url(ref e) => Some(e),
            Error::Request(ref e) => Some(e),

            #[cfg(feature = "chrono")]
            Error::ParseTimeLike { ref original_err, .. } => match original_err {
                Some(ref e) => Some(e),
                None => None,
            },

            _ => None,
        }
    }
}

impl Error {

    fn description(&self) -> String {
        match *self {
            Error::Json(ref e) => String::from(e.description()),

            Error::Authorization(_) => String::from(
                "Auth key was provided in an invalid format for a header."
            ),

            Error::Url(_) => String::from("Invalid URL was given/built."),

            Error::Ratelimited { limit, ref time_until_reset, .. } => {
                let lim_part = match limit {
                    Some(lim) => format!(" Limit of {} requests/min.", lim),
                    None => String::from(""),
                };

                let time_part = match *time_until_reset {
                    Some(ref timeur) => format!(" Resets at timestamp {}.", timeur),
                    None => String::from(""),
                };

                let dot = {
                    if limit.is_none() && time_until_reset.is_none() {
                        "."
                    } else {
                        ":"
                    }
                };

                format!("Ratelimited{}{}{}", dot, lim_part, time_part)
            },

            Error::Request(ref e) => String::from(e.description()),

//            Error::Decode(msg, _) => String::from(msg),

            Error::Status(ref status, _, _) => String::from(
                status.canonical_reason().unwrap_or(
                    "Unknown HTTP status code error received"
                )
            ),

            Error::FetchFrom(ref string) => string.clone(),

            #[cfg(feature = "chrono")]
            Error::ParseTimeLike {
                ref reason,
                ..
            } => reason.clone(),
        }
    }

    /// Obtain an Error from a Response (blocking). Optionally specify a pre-parsed JsonValue
    /// for the body, otherwise that parsing will be done inside this function.
    #[doc(hidden)]
    pub(crate) fn from_response(response: Response, value: Option<JsonValue>) -> Error {
        let status = response.status();

        let headers: &HeaderMap = response.headers();
        let headers = headers.clone();

        let value: Option<JsonValue> = match value {
            Some(val) => Some(val),
            None => serde_json::from_reader(response).ok()
        };

        let reset_header = headers.get("x-ratelimit-reset");
        if let Some(reset_header) = reset_header {  // ratelimited
            let reset_header = reset_header.to_str();
            if let Ok(reset) = reset_header {
                return Error::Ratelimited {
                    limit: match headers.get("x-ratelimit-limit") {
                        Some(lim_header) => lim_header.to_str().ok().and_then(
                            |s| { s.parse().ok() }
                        ),
                        None => None,
                    },

                    remaining: match headers.get("x-ratelimit-remaining") {
                        Some(rem_header) => rem_header.to_str().ok().and_then(
                            |s| { s.parse().ok() }
                        ),
                        None => None,
                    },

                    time_until_reset: Some(String::from(reset)),
                }
            }
        }

        let api_error: Option<APIError> = match value {
            Some(ref val) => serde_json::from_value(val.clone()).ok(),
            None => None,
        };

        Error::Status(status, api_error, value)
    }

    /// Obtain an Error from a Response (non-blocking). Optionally specify a pre-parsed JsonValue
    /// for the body, otherwise that parsing will be done inside this function.
    #[doc(hidden)]
    #[cfg(feature = "async")]
    pub(crate) async fn a_from_response(response: AResponse, value: Option<JsonValue>) -> Error {
        let status = response.status();
        let headers: &HeaderMap = response.headers();
        let headers = headers.clone();

        let value: Option<JsonValue> = match value {
            Some(val) => Some(val),
            None => response.json().await.ok()
        };

        let reset_header = headers.get("x-ratelimit-reset");
        if let Some(reset_header) = reset_header {  // ratelimited
            let reset_header = reset_header.to_str();
            if let Ok(reset) = reset_header {
                return Error::Ratelimited {
                    limit: match headers.get("x-ratelimit-limit") {
                        Some(lim_header) => lim_header.to_str().ok().and_then(
                            |s| { s.parse().ok() }
                        ),
                        None => None,
                    },

                    remaining: match headers.get("x-ratelimit-remaining") {
                        Some(rem_header) => rem_header.to_str().ok().and_then(
                            |s| { s.parse().ok() }
                        ),
                        None => None,
                    },

                    time_until_reset: Some(String::from(reset)),
                }
            }
        }

        let api_error: Option<APIError> = match value {
            Some(ref val) => serde_json::from_value(val.clone()).ok(),
            None => None,
        };

        Error::Status(status, api_error, value)
    }


}