reevit 0.2.0

Official Rust SDK for the Reevit payments API
Documentation
use reqwest::StatusCode;
use serde_json::Value;

/// A result returned by the Reevit SDK.
pub type Result<T> = std::result::Result<T, Error>;

/// An error returned by the Reevit API.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiError {
    /// HTTP status returned by Reevit.
    pub status: StatusCode,
    /// Stable machine-readable error code, when supplied.
    pub code: Option<String>,
    /// Human-readable error message.
    pub message: String,
    /// Additional structured context supplied by Reevit.
    pub details: Option<Value>,
    /// Request identifier useful when contacting support.
    pub request_id: Option<String>,
}

impl ApiError {
    /// Returns whether retrying the request may succeed.
    #[must_use]
    pub fn is_recoverable(&self) -> bool {
        matches!(self.status.as_u16(), 408 | 409 | 425 | 429) || self.status.is_server_error()
    }
}

impl std::fmt::Display for ApiError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.code {
            Some(code) => write!(
                formatter,
                "Reevit request failed with status {} ({code}): {}",
                self.status.as_u16(),
                self.message
            ),
            None => write!(
                formatter,
                "Reevit request failed with status {}: {}",
                self.status.as_u16(),
                self.message
            ),
        }
    }
}

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

/// Errors produced while configuring or calling Reevit.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// The client configuration is invalid.
    #[error("invalid Reevit client configuration: {0}")]
    Configuration(String),
    /// The HTTP request could not be completed.
    #[error("Reevit transport error: {0}")]
    Transport(TransportError),
    /// Reevit returned a non-successful HTTP response.
    #[error(transparent)]
    Api(#[from] ApiError),
    /// A successful response did not match the expected schema.
    #[error("failed to decode Reevit response (status {status}): {source}")]
    Decode {
        /// HTTP status of the response that failed to decode.
        status: StatusCode,
        /// JSON decoding failure.
        #[source]
        source: serde_json::Error,
    },
    /// A successful response omitted the resource shape required by the interface.
    #[error("unexpected Reevit response: {0}")]
    UnexpectedResponse(String),
}

/// A URL-safe summary of a failed HTTP operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransportError {
    kind: TransportErrorKind,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TransportErrorKind {
    Timeout,
    Connection,
    Request,
    Body,
    Other,
}

impl TransportError {
    /// Returns whether the operation exceeded its configured timeout.
    #[must_use]
    pub fn is_timeout(&self) -> bool {
        self.kind == TransportErrorKind::Timeout
    }

    /// Returns whether the SDK could not establish a network connection.
    #[must_use]
    pub fn is_connect(&self) -> bool {
        self.kind == TransportErrorKind::Connection
    }
}

impl std::fmt::Display for TransportError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let message = match self.kind {
            TransportErrorKind::Timeout => "request timed out",
            TransportErrorKind::Connection => "failed to connect to Reevit",
            TransportErrorKind::Request => "failed to construct or send the request",
            TransportErrorKind::Body => "failed while reading the response",
            TransportErrorKind::Other => "HTTP operation failed",
        };
        formatter.write_str(message)
    }
}

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

impl From<reqwest::Error> for Error {
    fn from(error: reqwest::Error) -> Self {
        let kind = if error.is_timeout() {
            TransportErrorKind::Timeout
        } else if error.is_connect() {
            TransportErrorKind::Connection
        } else if error.is_request() {
            TransportErrorKind::Request
        } else if error.is_body() || error.is_decode() {
            TransportErrorKind::Body
        } else {
            TransportErrorKind::Other
        };
        Self::Transport(TransportError { kind })
    }
}

impl Error {
    /// Returns whether retrying the request may succeed.
    #[must_use]
    pub fn is_recoverable(&self) -> bool {
        match self {
            Self::Transport(_) => true,
            Self::Api(error) => error.is_recoverable(),
            Self::Configuration(_) | Self::Decode { .. } | Self::UnexpectedResponse(_) => false,
        }
    }
}