use std::fmt;
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: u16,
pub code: Option<String>,
pub message: Option<String>,
pub details: String,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.details)
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Api(ApiError),
#[error(transparent)]
Http(#[from] reqwest::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("XML parse error: {0}")]
Xml(#[from] quick_xml::DeError),
#[error("XML serialize error: {0}")]
XmlSerialize(#[from] quick_xml::SeError),
#[error("invalid header value: {0}")]
Header(#[from] http::header::ToStrError),
#[error("invalid HTTP method: {0}")]
Method(#[from] http::method::InvalidMethod),
#[error("invalid URL: {0}")]
Url(#[from] url::ParseError),
#[error("invalid header name: {0}")]
HeaderName(#[from] http::header::InvalidHeaderName),
#[error("invalid header value: {0}")]
HeaderValue(#[from] http::header::InvalidHeaderValue),
#[error("{0}")]
Other(String),
}
impl Error {
#[must_use]
pub fn code(&self) -> Option<&str> {
match self {
Self::Api(e) => e.code.as_deref(),
_ => None,
}
}
#[must_use]
pub const fn status(&self) -> Option<u16> {
match self {
Self::Api(e) => Some(e.status),
_ => None,
}
}
#[must_use]
pub fn is_not_found(&self) -> bool {
match self {
Self::Api(e) => e.status == 404 || e.code.as_deref() == Some("NoSuchKey"),
_ => false,
}
}
}
impl From<anyhow::Error> for Error {
fn from(e: anyhow::Error) -> Self {
Self::Other(format!("{e:#}"))
}
}
impl From<tokio::task::JoinError> for Error {
fn from(e: tokio::task::JoinError) -> Self {
Self::Other(format!("background task failed: {e}"))
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
#[test]
fn test_api_error_display_and_accessors() {
let err = Error::Api(ApiError {
status: 404,
code: Some("NoSuchKey".to_string()),
message: Some("The specified key does not exist.".to_string()),
details: "HTTP Status Code: 404 Not Found\nCode: NoSuchKey\n".to_string(),
});
assert_eq!(err.status(), Some(404));
assert_eq!(err.code(), Some("NoSuchKey"));
assert!(err.is_not_found());
assert!(err.to_string().contains("NoSuchKey"));
}
#[test]
fn test_other_has_no_code_or_status() {
let err = Error::Other("boom".to_string());
assert_eq!(err.code(), None);
assert_eq!(err.status(), None);
assert!(!err.is_not_found());
assert_eq!(err.to_string(), "boom");
}
#[test]
fn test_from_anyhow() {
let err: Error = anyhow::anyhow!("internal failure").into();
assert!(matches!(err, Error::Other(_)));
assert!(err.to_string().contains("internal failure"));
}
}