use std::fmt;
use reqwest::{Method, StatusCode};
pub type APIResult<O, E> = Result<O, Error<E>>;
#[allow(clippy::module_name_repetitions)]
pub type NoSpecificError = std::convert::Infallible;
#[derive(Debug, thiserror::Error)]
#[allow(clippy::module_name_repetitions)]
pub enum ErrorKind<T> {
#[error("failed to send request: {0}")]
Request(#[source] reqwest::Error),
#[error("received error response from server: {0}")]
Response(ResponseError),
#[error("failed to parse JSON response: {0}")]
ParseJSON(#[source] reqwest::Error),
#[error(transparent)]
Other(T),
}
impl<T> ErrorKind<T> {
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)),
}
}
}
#[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 {
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))
}
pub fn method(&self) -> &Method {
&self.method
}
pub fn url(&self) -> &str {
&self.url
}
pub fn inner(&self) -> &ErrorKind<T> {
&self.inner
}
}
#[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)
}
}