use std::fmt;
use reqwest::header::HeaderMap;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: u16,
pub code: Option<String>,
pub message: String,
pub details: Option<Value>,
pub request_id: Option<String>,
pub retry_after: Option<u64>,
}
impl ApiError {
pub fn is_auth(&self) -> bool {
self.status == 401
}
pub fn is_permission(&self) -> bool {
self.status == 403
}
pub fn is_not_found(&self) -> bool {
self.status == 404
}
pub fn is_validation(&self) -> bool {
self.status == 422
}
pub fn is_rate_limited(&self) -> bool {
self.status == 429
}
pub fn is_server(&self) -> bool {
self.status >= 500
}
}
#[derive(Debug)]
pub enum Error {
Api(ApiError),
Timeout,
Connection(String),
Decode(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Api(e) => write!(f, "bitculator: HTTP {}: {}", e.status, e.message),
Error::Timeout => write!(f, "bitculator: request timed out"),
Error::Connection(m) => write!(f, "bitculator: network error: {m}"),
Error::Decode(m) => write!(f, "bitculator: failed to decode response: {m}"),
}
}
}
impl std::error::Error for Error {}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
if e.is_timeout() {
Error::Timeout
} else if e.is_decode() {
Error::Decode(e.to_string())
} else {
Error::Connection(e.to_string())
}
}
}
pub(crate) fn build_api_error(status: u16, body: &Value, headers: &HeaderMap) -> ApiError {
let mut code = None;
let mut message = format!("HTTP {status}");
let mut details = None;
if let Some(err) = body.get("error") {
if let Some(c) = err.get("code").and_then(Value::as_str) {
code = Some(c.to_string());
}
if let Some(m) = err.get("message").and_then(Value::as_str) {
message = m.to_string();
}
details = err.get("details").cloned();
} else if let Some(m) = body.get("message").and_then(Value::as_str) {
message = m.to_string();
}
let request_id = headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(String::from);
let retry_after = if status == 429 {
headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
} else {
None
};
ApiError { status, code, message, details, request_id, retry_after }
}