use thiserror::Error;
pub type Result<T> = std::result::Result<T, CrateCheckerError>;
#[derive(Error, Debug)]
pub enum CrateCheckerError {
#[error("HTTP request failed: {0}")]
HttpError(#[from] reqwest::Error),
#[error("JSON parsing failed: {0}")]
JsonError(#[from] serde_json::Error),
#[error("YAML parsing failed: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Configuration error: {0}")]
ConfigError(#[from] config::ConfigError),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Crate '{0}' not found")]
CrateNotFound(String),
#[error("Version '{version}' not found for crate '{crate_name}'")]
VersionNotFound { crate_name: String, version: String },
#[error("Invalid crate name: '{0}'. Crate names must match the pattern: {1}")]
InvalidCrateName(String, &'static str),
#[error("API rate limit exceeded. Please try again later")]
RateLimitExceeded,
#[error("Server error: {status} - {message}")]
ServerError { status: u16, message: String },
#[error("Request timeout after {0} seconds")]
Timeout(u64),
#[error("Batch processing failed: {0}")]
BatchError(String),
#[error("Invalid batch input: {0}")]
InvalidBatchInput(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Application error: {0}")]
ApplicationError(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Authentication failed: {0}")]
AuthError(String),
#[error("Service temporarily unavailable: {0}")]
ServiceUnavailable(String),
}
impl CrateCheckerError {
pub fn application<S: Into<String>>(message: S) -> Self {
Self::ApplicationError(message.into())
}
pub fn validation<S: Into<String>>(message: S) -> Self {
Self::ValidationError(message.into())
}
pub fn batch<S: Into<String>>(message: S) -> Self {
Self::BatchError(message.into())
}
pub fn network<S: Into<String>>(message: S) -> Self {
Self::NetworkError(message.into())
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::HttpError(_)
| Self::NetworkError(_)
| Self::Timeout(_)
| Self::ServiceUnavailable(_)
| Self::RateLimitExceeded
)
}
pub fn status_code(&self) -> Option<u16> {
match self {
Self::ServerError { status, .. } => Some(*status),
Self::CrateNotFound(_) | Self::VersionNotFound { .. } => Some(404),
Self::RateLimitExceeded => Some(429),
Self::AuthError(_) => Some(401),
Self::ValidationError(_) | Self::InvalidCrateName(_, _) => Some(400),
Self::ServiceUnavailable(_) => Some(503),
_ => None,
}
}
pub fn user_message(&self) -> String {
match self {
Self::CrateNotFound(name) => {
format!("The crate '{}' does not exist on crates.io", name)
}
Self::VersionNotFound {
crate_name,
version,
} => {
format!(
"Version '{}' of crate '{}' was not found",
version, crate_name
)
}
Self::InvalidCrateName(name, pattern) => {
format!(
"'{}' is not a valid crate name. Names must match: {}",
name, pattern
)
}
Self::RateLimitExceeded => {
"You've exceeded the API rate limit. Please wait a moment before trying again."
.to_string()
}
Self::NetworkError(_) => {
"Network connection failed. Please check your internet connection.".to_string()
}
Self::ServiceUnavailable(_) => {
"The crates.io service is temporarily unavailable. Please try again later."
.to_string()
}
_ => self.to_string(),
}
}
}
impl From<reqwest::StatusCode> for CrateCheckerError {
fn from(status: reqwest::StatusCode) -> Self {
match status.as_u16() {
404 => Self::ValidationError("Resource not found".to_string()),
429 => Self::RateLimitExceeded,
500..=599 => Self::ServiceUnavailable(format!("Server error: {}", status)),
_ => Self::ServerError {
status: status.as_u16(),
message: status
.canonical_reason()
.unwrap_or("Unknown error")
.to_string(),
},
}
}
}