use std::{collections::BTreeMap, fmt, time::Duration};
use serde_json::Value;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EzError {
pub request_id: Option<String>,
pub task_id: Option<String>,
pub error_code: Option<String>,
pub error_description: Option<String>,
pub errors: BTreeMap<String, String>,
pub http_status: u16,
}
const AUTHENTICATION_ERROR_CODES: &[&str] = &[
"ERROR_KEY_DOES_NOT_EXIST",
"ERROR_KEY_NOT_AVAILABLE",
"ERROR_ZERO_BALANCE",
];
const TERMINAL_ERROR_CODES: &[&str] = &[
"ERROR_CONTENT_TYPE_ERROR",
"ERROR_KEY_DOES_NOT_EXIST",
"ERROR_KEY_NOT_AVAILABLE",
"ERROR_NOT_FOUND",
"ERROR_PACKAGE_NOT_EXIST",
"ERROR_PACKAGE_TASK_TYPE_NOT_SUPPORTED",
"ERROR_REQUEST_METHOD",
"ERROR_REQUEST_PARAMETERS",
"ERROR_REQUEST_PROXY_MISSING",
"ERROR_SUBSCRIPTION_EXPIRED",
"ERROR_TASK_NOT_EXIST",
"ERROR_TASK_TYPE_NOT_ALLOWED",
"ERROR_TASK_TYPE_NOT_AVAILABLE",
"ERROR_TASK_TYPE_NOT_SUPPORTED",
"ERROR_WEBSITE_NOT_ALLOWED",
"ERROR_ZERO_BALANCE",
];
const RATE_LIMITED_ERROR_CODES: &[&str] = &["ERROR_REQUEST_LIMIT", "ERROR_REQUEST_BANNED"];
impl EzError {
#[must_use]
pub fn is_authentication_error(&self) -> bool {
self.error_code
.as_deref()
.is_some_and(|code| AUTHENTICATION_ERROR_CODES.contains(&code))
}
#[must_use]
pub fn is_terminal(&self) -> bool {
self.error_code
.as_deref()
.is_some_and(|code| TERMINAL_ERROR_CODES.contains(&code))
}
#[must_use]
pub fn is_rate_limited(&self) -> bool {
self.error_code
.as_deref()
.is_some_and(|code| RATE_LIMITED_ERROR_CODES.contains(&code))
}
}
impl fmt::Display for EzError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = self.error_code.as_deref().unwrap_or("UNKNOWN_API_ERROR");
let description = self
.error_description
.as_deref()
.unwrap_or("The API returned an unspecified error");
write!(
formatter,
"{code}: {description} (HTTP {})",
self.http_status
)?;
for (index, (field, message)) in self.errors.iter().enumerate() {
let separator = if index == 0 { "; " } else { ", " };
write!(formatter, "{separator}{field}: {message}")?;
}
Ok(())
}
}
impl std::error::Error for EzError {}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{0}")]
Message(String),
#[cfg(any(feature = "async", feature = "blocking"))]
#[error("HTTP request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("unexpected API response: {0}")]
UnexpectedResponse(String),
#[error(transparent)]
EzError(Box<EzError>),
#[error(
"task `{task_id}` did not complete after {attempts} polling attempts at {interval:?} intervals"
)]
PollingExhausted {
task_id: String,
attempts: usize,
interval: Duration,
},
#[error("task `{task_id}` was created but waiting for its result failed: {source}")]
WaitInterrupted {
task_id: String,
request_id: Option<String>,
#[source]
source: Box<Error>,
},
#[error("failed to decode task solution: {source}; raw solution: {}", preview_solution(.raw))]
SolutionDecode {
#[source]
source: serde_json::Error,
raw: Box<Value>,
},
}
fn preview_solution(value: &Value) -> String {
const MAX_CHARS: usize = 512;
let rendered = value.to_string();
let mut chars = rendered.chars();
let head: String = chars.by_ref().take(MAX_CHARS).collect();
if chars.next().is_some() {
format!("{head}...")
} else {
head
}
}
impl Error {
#[must_use]
pub fn task_id(&self) -> Option<&str> {
match self {
Self::WaitInterrupted { task_id, .. } | Self::PollingExhausted { task_id, .. } => {
Some(task_id)
}
Self::EzError(error) => error.task_id.as_deref(),
_ => None,
}
}
}
impl From<EzError> for Error {
fn from(error: EzError) -> Self {
Self::EzError(Box::new(error))
}
}
#[cfg(any(feature = "async", feature = "blocking"))]
impl Error {
pub(crate) fn config(detail: impl fmt::Display) -> Self {
Self::Message(format!("invalid client configuration: {detail}"))
}
pub(crate) fn with_task_context(self, task_id: String, request_id: Option<String>) -> Self {
match self {
Self::EzError(mut error) => {
error.task_id = Some(task_id);
error.request_id = request_id;
Self::EzError(error)
}
already_identified @ (Self::PollingExhausted { .. } | Self::WaitInterrupted { .. }) => {
already_identified
}
source => Self::WaitInterrupted {
task_id,
request_id,
source: Box::new(source),
},
}
}
}