bunny_api/
error.rs

1use std::fmt;
2
3use reqwest::{Method, StatusCode};
4
5/// The result type returned by API calls.
6pub type APIResult<O, E> = Result<O, Error<E>>;
7
8/// Type alias for [`Infallible`](std::convert::Infallible) that better indicates
9/// that errors can still happen, but not [`ErrorKind::Other`].
10#[allow(clippy::module_name_repetitions)]
11pub type NoSpecificError = std::convert::Infallible;
12
13/// Possible kinds of errors returned by an API function.
14///
15/// # Note
16///
17/// API calls may inspect request or response errors to return a more specific error in `Other`.
18/// That is, a server's error response may apper in `Response` or in `Other`, depending on if the
19/// API function interprets the response body.
20#[derive(Debug, thiserror::Error)]
21#[allow(clippy::module_name_repetitions)]
22pub enum ErrorKind<T> {
23    /// A request failed to send.
24    #[error("failed to send request: {0}")]
25    Request(#[source] reqwest::Error),
26    /// Received an error response (`4xx` or `5xx`) from the server.
27    #[error("received error response from server: {0}")]
28    Response(ResponseError),
29    /// Failed to parse JSON response.
30    #[error("failed to parse JSON response: {0}")]
31    ParseJSON(#[source] reqwest::Error),
32    /// Some other, more specific error.
33    #[error(transparent)]
34    Other(T),
35}
36
37impl<T> ErrorKind<T> {
38    /// Convert the contained `T` error, if present.
39    pub fn map<F, U>(self, f: F) -> ErrorKind<U>
40        where F: FnOnce(T) -> U,
41    {
42        match self {
43            Self::Request(err) => ErrorKind::Request(err),
44            Self::Response(err) => ErrorKind::Response(err),
45            Self::ParseJSON(err) => ErrorKind::ParseJSON(err),
46            Self::Other(err) => ErrorKind::Other(f(err)),
47        }
48    }
49}
50
51/// An error returned by an API call.
52///
53/// The error includes the HTTP Method and URL of the API call, regardless of whether the request
54/// was actually sent to the server.
55#[derive(Debug, thiserror::Error)]
56#[error("failed to {method} to {url:?}: {inner}")]
57pub struct Error<T> where T: std::error::Error + 'static {
58    #[source]
59    inner: ErrorKind<T>,
60    url: String,
61    method: Method,
62}
63
64impl<T> Error<T> where T: std::error::Error + 'static {
65    /// Convert the contained `T` error, if present.
66    pub fn map<F, U>(self, f: F) -> Error<U>
67        where F: FnOnce(T) -> U,
68              U: std::error::Error + 'static,
69    {
70        let Self { inner, method, url } = self;
71        let inner = inner.map(f);
72        Error::inner_new(method, url, inner)
73    }
74
75    pub(crate) fn map_err(method: Method, url: String) -> impl FnOnce(T) -> Self {
76        move |inner| Self::new(method, url, inner)
77    }
78
79    pub(crate) fn map_json_err(method: Method, url: String) -> impl FnOnce(reqwest::Error) -> Self {
80        move |error| Self::from_json(method, url, error)
81    }
82
83    pub(crate) fn map_request_err(method: Method, url: String) -> impl FnOnce(reqwest::Error) -> Self {
84        move |error| Self::from_request(method, url, error)
85    }
86
87    pub(crate) fn map_response_err(method: Method, url: String) -> impl FnOnce(ResponseError) -> Self {
88        move |error| Self::from_response(method, url, error)
89    }
90
91    fn inner_new(method: Method, url: String, inner: ErrorKind<T>) -> Self {
92        Self { inner, url, method }
93    }
94
95    pub(crate) fn new(method: Method, url: String, inner: T) -> Self {
96        Self::inner_new(method, url, ErrorKind::Other(inner))
97    }
98
99    pub(crate) fn from_json(method: Method, url: String, inner: reqwest::Error) -> Self {
100        Self::inner_new(method, url, ErrorKind::ParseJSON(inner))
101    }
102
103    pub(crate) fn from_request(method: Method, url: String, inner: reqwest::Error) -> Self {
104        Self::inner_new(method, url, ErrorKind::Request(inner))
105    }
106
107    pub(crate) fn from_response(method: Method, url: String, inner: ResponseError) -> Self {
108        Self::inner_new(method, url, ErrorKind::Response(inner))
109    }
110
111    /// Returns the HTTP Method of the failed request.
112    ///
113    /// Note that the request may not have happened before the error occurred. In this case,
114    /// this merely indicates what method the resulting API call *would* have used.
115    pub fn method(&self) -> &Method {
116        &self.method
117    }
118
119    /// Returns the URL of the failed request.
120    ///
121    /// Note that the request may not have happened before the error occurred. In this case,
122    /// this is merely the URL that the resulting API call *would* have gone to.
123    pub fn url(&self) -> &str {
124        &self.url
125    }
126
127    /// Return the inner error.
128    pub fn inner(&self) -> &ErrorKind<T> {
129        &self.inner
130    }
131}
132
133/// The server returned an error response code (`4xx` or `5xx`).
134#[derive(Debug)]
135#[allow(clippy::module_name_repetitions)]
136pub struct ResponseError {
137    status: StatusCode,
138    body: Option<String>,
139}
140
141impl fmt::Display for ResponseError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "failed with status {}", self.status)?;
144        if let Some(body) = &self.body {
145            if !body.is_empty() {
146                write!(f, ": {}", body)?;
147            }
148        }
149        Ok(())
150    }
151}
152
153impl std::error::Error for ResponseError {}
154
155pub(crate) async fn error_from_response(resp: reqwest::Response) -> Result<reqwest::Response, ResponseError> {
156    if resp.status().is_client_error() || resp.status().is_server_error() {
157        Err(ResponseError {
158            status: resp.status(),
159            body: resp.text().await.ok(),
160        })
161    } else {
162        Ok(resp)
163    }
164}