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
use std::fmt;

use reqwest::{Method, StatusCode};

/// The result type returned by API calls.
pub type APIResult<O, E> = Result<O, Error<E>>;

/// Type alias for [`Infallible`](std::convert::Infallible) that better indicates
/// that errors can still happen, but not [`ErrorKind::Other`].
#[allow(clippy::module_name_repetitions)]
pub type NoSpecificError = std::convert::Infallible;

/// Possible kinds of errors returned by an API function.
///
/// # Note
///
/// API calls may inspect request or response errors to return a more specific error in `Other`.
/// That is, a server's error response may apper in `Response` or in `Other`, depending on if the
/// API function interprets the response body.
#[derive(Debug, thiserror::Error)]
#[allow(clippy::module_name_repetitions)]
pub enum ErrorKind<T> {
    /// A request failed to send.
    #[error("failed to send request: {0}")]
    Request(#[source] reqwest::Error),
    /// Received an error response (`4xx` or `5xx`) from the server.
    #[error("received error response from server: {0}")]
    Response(ResponseError),
    /// Failed to parse JSON response.
    #[error("failed to parse JSON response: {0}")]
    ParseJSON(#[source] reqwest::Error),
    /// Some other, more specific error.
    #[error(transparent)]
    Other(T),
}

impl<T> ErrorKind<T> {
    /// Convert the contained `T` error, if present.
    pub fn map<F, U>(self, f: F) -> ErrorKind<U>
        where F: FnOnce(T) -> U,
    {
        match self {
            Self::Request(err) => ErrorKind::Request(err),
            Self::Response(err) => ErrorKind::Response(err),
            Self::ParseJSON(err) => ErrorKind::ParseJSON(err),
            Self::Other(err) => ErrorKind::Other(f(err)),
        }
    }
}

/// An error returned by an API call.
///
/// The error includes the HTTP Method and URL of the API call, regardless of whether the request
/// was actually sent to the server.
#[derive(Debug, thiserror::Error)]
#[error("failed to {method} to {url:?}: {inner}")]
pub struct Error<T> where T: std::error::Error + 'static {
    #[source]
    inner: ErrorKind<T>,
    url: String,
    method: Method,
}

impl<T> Error<T> where T: std::error::Error + 'static {
    /// Convert the contained `T` error, if present.
    pub fn map<F, U>(self, f: F) -> Error<U>
        where F: FnOnce(T) -> U,
              U: std::error::Error + 'static,
    {
        let Self { inner, method, url } = self;
        let inner = inner.map(f);
        Error::inner_new(method, url, inner)
    }

    pub(crate) fn map_err(method: Method, url: String) -> impl FnOnce(T) -> Self {
        move |inner| Self::new(method, url, inner)
    }

    pub(crate) fn map_json_err(method: Method, url: String) -> impl FnOnce(reqwest::Error) -> Self {
        move |error| Self::from_json(method, url, error)
    }

    pub(crate) fn map_request_err(method: Method, url: String) -> impl FnOnce(reqwest::Error) -> Self {
        move |error| Self::from_request(method, url, error)
    }

    pub(crate) fn map_response_err(method: Method, url: String) -> impl FnOnce(ResponseError) -> Self {
        move |error| Self::from_response(method, url, error)
    }

    fn inner_new(method: Method, url: String, inner: ErrorKind<T>) -> Self {
        Self { inner, url, method }
    }

    pub(crate) fn new(method: Method, url: String, inner: T) -> Self {
        Self::inner_new(method, url, ErrorKind::Other(inner))
    }

    pub(crate) fn from_json(method: Method, url: String, inner: reqwest::Error) -> Self {
        Self::inner_new(method, url, ErrorKind::ParseJSON(inner))
    }

    pub(crate) fn from_request(method: Method, url: String, inner: reqwest::Error) -> Self {
        Self::inner_new(method, url, ErrorKind::Request(inner))
    }

    pub(crate) fn from_response(method: Method, url: String, inner: ResponseError) -> Self {
        Self::inner_new(method, url, ErrorKind::Response(inner))
    }

    /// Returns the HTTP Method of the failed request.
    ///
    /// Note that the request may not have happened before the error occurred. In this case,
    /// this merely indicates what method the resulting API call *would* have used.
    pub fn method(&self) -> &Method {
        &self.method
    }

    /// Returns the URL of the failed request.
    ///
    /// Note that the request may not have happened before the error occurred. In this case,
    /// this is merely the URL that the resulting API call *would* have gone to.
    pub fn url(&self) -> &str {
        &self.url
    }

    /// Return the inner error.
    pub fn inner(&self) -> &ErrorKind<T> {
        &self.inner
    }
}

/// The server returned an error response code (`4xx` or `5xx`).
#[derive(Debug)]
#[allow(clippy::module_name_repetitions)]
pub struct ResponseError {
    status: StatusCode,
    body: Option<String>,
}

impl fmt::Display for ResponseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "failed with status {}", self.status)?;
        if let Some(body) = &self.body {
            if !body.is_empty() {
                write!(f, ": {}", body)?;
            }
        }
        Ok(())
    }
}

impl std::error::Error for ResponseError {}

pub(crate) async fn error_from_response(resp: reqwest::Response) -> Result<reqwest::Response, ResponseError> {
    if resp.status().is_client_error() || resp.status().is_server_error() {
        Err(ResponseError {
            status: resp.status(),
            body: resp.text().await.ok(),
        })
    } else {
        Ok(resp)
    }
}