use reqwest::StatusCode;
use serde_json::Value;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiError {
pub status: StatusCode,
pub code: Option<String>,
pub message: String,
pub details: Option<Value>,
pub request_id: Option<String>,
}
impl ApiError {
#[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 {}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid Reevit client configuration: {0}")]
Configuration(String),
#[error("Reevit transport error: {0}")]
Transport(TransportError),
#[error(transparent)]
Api(#[from] ApiError),
#[error("failed to decode Reevit response (status {status}): {source}")]
Decode {
status: StatusCode,
#[source]
source: serde_json::Error,
},
#[error("unexpected Reevit response: {0}")]
UnexpectedResponse(String),
}
#[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 {
#[must_use]
pub fn is_timeout(&self) -> bool {
self.kind == TransportErrorKind::Timeout
}
#[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 {
#[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,
}
}
}