bunny-api 0.0.5

Alpha API client for Bunny.net
Documentation
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)
    }
}