use serde_json::Value;
use crate::models::{Job, JobMessage};
pub type Result<T> = std::result::Result<T, Api2ConvertError>;
#[derive(Debug, Clone)]
pub struct ApiErrorData {
pub status: u16,
pub message: String,
pub request_id: Option<String>,
pub body: Value,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Api2ConvertError {
Config(String),
Network(String),
Authentication(ApiErrorData),
PaymentRequired(ApiErrorData),
NotFound(ApiErrorData),
Validation(ApiErrorData),
RateLimit {
data: ApiErrorData,
retry_after: Option<i64>,
},
Server(ApiErrorData),
Api(ApiErrorData),
ConversionFailed {
job: Box<Job>,
errors: Vec<JobMessage>,
},
ConversionTimeout { job: Box<Job> },
SignatureVerification(String),
}
impl Api2ConvertError {
pub(crate) fn api(
status: u16,
message: String,
request_id: Option<String>,
body: Value,
) -> Self {
let data = ApiErrorData {
status,
message,
request_id,
body,
};
match status {
401 | 403 => Api2ConvertError::Authentication(data),
402 => Api2ConvertError::PaymentRequired(data),
404 => Api2ConvertError::NotFound(data),
400 | 422 => Api2ConvertError::Validation(data),
429 => Api2ConvertError::RateLimit {
data,
retry_after: None,
},
500..=599 => Api2ConvertError::Server(data),
_ => Api2ConvertError::Api(data),
}
}
fn http_data(&self) -> Option<&ApiErrorData> {
match self {
Api2ConvertError::Authentication(d)
| Api2ConvertError::PaymentRequired(d)
| Api2ConvertError::NotFound(d)
| Api2ConvertError::Validation(d)
| Api2ConvertError::Server(d)
| Api2ConvertError::Api(d) => Some(d),
Api2ConvertError::RateLimit { data, .. } => Some(data),
_ => None,
}
}
pub fn status(&self) -> Option<u16> {
self.http_data().map(|d| d.status)
}
pub fn request_id(&self) -> Option<&str> {
self.http_data().and_then(|d| d.request_id.as_deref())
}
pub fn body(&self) -> Option<&Value> {
self.http_data().map(|d| &d.body)
}
pub fn retry_after(&self) -> Option<i64> {
match self {
Api2ConvertError::RateLimit { retry_after, .. } => *retry_after,
_ => None,
}
}
}
impl std::fmt::Display for Api2ConvertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Api2ConvertError::Config(m) => write!(f, "configuration error: {m}"),
Api2ConvertError::Network(m) => write!(f, "network error: {m}"),
Api2ConvertError::Authentication(d) => {
write!(f, "authentication error ({}): {}", d.status, d.message)
}
Api2ConvertError::PaymentRequired(d) => {
write!(f, "payment required ({}): {}", d.status, d.message)
}
Api2ConvertError::NotFound(d) => write!(f, "not found ({}): {}", d.status, d.message),
Api2ConvertError::Validation(d) => {
write!(f, "validation error ({}): {}", d.status, d.message)
}
Api2ConvertError::RateLimit { data, retry_after } => match retry_after {
Some(s) => write!(
f,
"rate limited ({}): {} (retry after {}s)",
data.status, data.message, s
),
None => write!(f, "rate limited ({}): {}", data.status, data.message),
},
Api2ConvertError::Server(d) => write!(f, "server error ({}): {}", d.status, d.message),
Api2ConvertError::Api(d) => write!(f, "API error ({}): {}", d.status, d.message),
Api2ConvertError::ConversionFailed { job, errors } => {
let detail = errors
.first()
.map(|e| e.message.clone())
.or_else(|| job.status.info.clone())
.unwrap_or_else(|| format!("status {}", job.status.code));
write!(f, "conversion failed (job {}): {}", job.id, detail)
}
Api2ConvertError::ConversionTimeout { job } => {
write!(
f,
"conversion timed out (job {}, last status {})",
job.id, job.status.code
)
}
Api2ConvertError::SignatureVerification(m) => {
write!(f, "signature verification failed: {m}")
}
}
}
}
impl std::error::Error for Api2ConvertError {}