use std::time::Duration;
use reqwest::header::HeaderMap;
use serde::Deserialize;
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct AniListErrorLocation {
pub line: i64,
pub column: i64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct AniListGqlError {
pub message: String,
#[serde(default)]
pub status: Option<i64>,
#[serde(default)]
pub locations: Vec<AniListErrorLocation>,
#[serde(default)]
pub path: Vec<serde_json::Value>,
#[serde(default)]
pub extensions: Option<serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AniListError {
#[error("HTTP transport error: {0}")]
Http(#[from] reqwest::Error),
#[error("AniList HTTP error {status}: {message}")]
Status {
status: u16,
retry_after: Option<Duration>,
message: String,
},
#[error("GraphQL errors: {}", .errors.iter().map(|e| e.message.as_str()).collect::<Vec<_>>().join("; "))]
GraphQL {
errors: Vec<AniListGqlError>,
retry_after: Option<Duration>,
},
#[error("deserialization error: {0}")]
Deserialization(#[from] serde_json::Error),
#[error("no data returned from AniList")]
NoData,
#[error("not found")]
NotFound,
#[error("invalid configuration: {0}")]
InvalidConfig(String),
}
impl AniListError {
pub(crate) fn is_retryable(&self) -> bool {
fn retryable_status(status: i64) -> bool {
status == 429 || (500..=599).contains(&status)
}
match self {
AniListError::Status { status, .. } => retryable_status(i64::from(*status)),
AniListError::GraphQL { errors, .. } => {
errors.iter().filter_map(|e| e.status).any(retryable_status)
}
AniListError::Http(e) => e.is_timeout() || e.is_connect(),
_ => false,
}
}
pub(crate) fn retry_after(&self) -> Option<Duration> {
match self {
AniListError::Status { retry_after, .. }
| AniListError::GraphQL { retry_after, .. } => *retry_after,
_ => None,
}
}
}
pub(crate) fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
let raw = headers.get(reqwest::header::RETRY_AFTER)?.to_str().ok()?;
raw.trim().parse::<u64>().ok().map(Duration::from_secs)
}