use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("API error (status {status}): {message}")]
Api {
status: u16,
message: String,
code: Option<String>,
},
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Configuration error: {0}")]
Config(String),
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("Rate limit exceeded, retry after {retry_after:?}")]
RateLimit {
retry_after: Option<std::time::Duration>,
},
#[error("Invalid URL: {0}")]
Url(#[from] url::ParseError),
#[error("Request timed out")]
Timeout,
#[error("Unknown error: {0}")]
Unknown(String),
}
impl Error {
pub fn is_retryable(&self) -> bool {
match self {
Error::Network(_) => true,
Error::Timeout => true,
Error::RateLimit { .. } => true,
Error::Api { status, .. } => *status >= 500 && *status < 600,
_ => false,
}
}
pub fn is_auth_error(&self) -> bool {
matches!(
self,
Error::Authentication(_)
| Error::Api {
status: 401 | 403,
..
}
)
}
pub(crate) fn api_error(status: u16, message: impl Into<String>) -> Self {
Error::Api {
status,
message: message.into(),
code: None,
}
}
pub(crate) fn api_error_with_code(
status: u16,
message: impl Into<String>,
code: impl Into<String>,
) -> Self {
Error::Api {
status,
message: message.into(),
code: Some(code.into()),
}
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_retryable() {
assert!(Error::Timeout.is_retryable());
assert!(Error::RateLimit { retry_after: None }.is_retryable());
assert!(Error::api_error(500, "Server error").is_retryable());
assert!(Error::api_error(503, "Service unavailable").is_retryable());
assert!(!Error::api_error(400, "Bad request").is_retryable());
assert!(!Error::api_error(404, "Not found").is_retryable());
assert!(!Error::Config("Invalid config".into()).is_retryable());
}
#[test]
fn test_is_auth_error() {
assert!(Error::Authentication("Invalid key".into()).is_auth_error());
assert!(Error::api_error(401, "Unauthorized").is_auth_error());
assert!(Error::api_error(403, "Forbidden").is_auth_error());
assert!(!Error::api_error(404, "Not found").is_auth_error());
assert!(!Error::Timeout.is_auth_error());
}
#[test]
fn test_error_display() {
let err = Error::api_error(404, "Customer not found");
assert_eq!(
err.to_string(),
"API error (status 404): Customer not found"
);
let err = Error::Config("Missing API key".into());
assert_eq!(err.to_string(), "Configuration error: Missing API key");
}
}