use serde::Deserialize;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{0}")]
Api(#[from] ApiError),
#[error("request failed: {0}")]
Transport(#[from] reqwest::Error),
#[error("could not decode the response body: {source}")]
Decode {
#[source]
source: serde_json::Error,
body: String,
},
#[error("{0}")]
Config(String),
}
impl Error {
pub fn status(&self) -> Option<u16> {
match self {
Error::Api(err) => Some(err.status),
Error::Transport(err) => err.status().map(|s| s.as_u16()),
_ => None,
}
}
pub fn code(&self) -> Option<&str> {
match self {
Error::Api(err) => err.code.as_deref(),
_ => None,
}
}
pub fn is_rate_limited(&self) -> bool {
self.status() == Some(429)
}
pub fn retry_after(&self) -> Option<f64> {
match self {
Error::Api(err) => err.retry_after,
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: u16,
pub code: Option<String>,
pub message: String,
pub body: Option<serde_json::Value>,
pub retry_after: Option<f64>,
}
impl ApiError {
pub fn upgrade_url(&self) -> Option<&str> {
self.body.as_ref()?.get("upgrade_url")?.as_str()
}
pub fn is_unauthorized(&self) -> bool {
self.status == 401
}
pub fn is_payment_required(&self) -> bool {
self.status == 402
}
pub fn is_forbidden(&self) -> bool {
self.status == 403
}
pub fn is_not_found(&self) -> bool {
self.status == 404
}
pub fn is_rate_limited(&self) -> bool {
self.status == 429
}
pub(crate) fn from_body(
status: u16,
body: Option<serde_json::Value>,
retry_after: Option<f64>,
) -> Self {
#[derive(Deserialize)]
struct Envelope {
error: Option<String>,
message: Option<String>,
}
let envelope = body
.clone()
.and_then(|value| serde_json::from_value::<Envelope>(value).ok());
let code = envelope.as_ref().and_then(|e| e.error.clone());
let message = envelope
.and_then(|e| e.message)
.filter(|m| !m.is_empty())
.or_else(|| code.clone())
.unwrap_or_else(|| format!("HTTP {status}"));
ApiError {
status,
code,
message,
body,
retry_after,
}
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.code {
Some(code) => write!(f, "[{} {}] {}", self.status, code, self.message),
None => write!(f, "[{}] {}", self.status, self.message),
}
}
}
impl std::error::Error for ApiError {}