use compact_str::CompactString;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ClientError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Failed to parse JSON response from {endpoint}: {message}")]
JsonParse {
endpoint: String,
message: String,
#[source]
source: serde_json::Error,
},
#[error("GitLab API error: {message}")]
GitlabApi { message: CompactString },
#[error("Configuration error: {0}")]
Config(String),
#[error("Invalid {field}: {message}")]
ConfigValidation { field: String, message: String },
#[error("Authentication failed")]
Authentication,
#[error("GitLab token is invalid")]
InvalidToken,
#[error("GitLab token has expired")]
ExpiredToken,
#[error("Request timeout")]
#[allow(dead_code)]
Timeout,
#[error("Invalid URL: {url}")]
#[allow(dead_code)]
InvalidUrl { url: String },
#[error("Resource not found: {resource}")]
NotFound { resource: String },
#[error("Rate limit exceeded, retry after {retry_after:?}")]
RateLimit { retry_after: Option<std::time::Duration> },
}
impl ClientError {
pub fn json_parse(
endpoint: impl Into<String>,
message: impl Into<String>,
source: serde_json::Error,
) -> Self {
Self::JsonParse {
endpoint: endpoint.into(),
message: message.into(),
source,
}
}
pub fn gitlab_api(message: impl Into<CompactString>) -> Self {
Self::GitlabApi { message: message.into() }
}
pub fn config(message: impl Into<String>) -> Self {
Self::Config(message.into())
}
pub fn config_validation(field: impl Into<String>, message: impl Into<String>) -> Self {
Self::ConfigValidation { field: field.into(), message: message.into() }
}
#[allow(dead_code)]
pub fn invalid_url(url: impl Into<String>) -> Self {
Self::InvalidUrl { url: url.into() }
}
pub fn not_found(resource: impl Into<String>) -> Self {
Self::NotFound { resource: resource.into() }
}
pub fn rate_limit(retry_after: Option<std::time::Duration>) -> Self {
Self::RateLimit { retry_after }
}
#[allow(dead_code)]
pub fn is_retryable(&self) -> bool {
match self {
ClientError::Http(e) => e.is_timeout() || e.is_connect(),
ClientError::Timeout => true,
ClientError::RateLimit { .. } => true,
_ => false,
}
}
#[allow(dead_code)]
pub fn is_network_error(&self) -> bool {
match self {
ClientError::Http(e) => e.is_timeout() || e.is_connect() || e.is_request(),
ClientError::Timeout => true,
_ => false,
}
}
}
pub type Result<T> = std::result::Result<T, ClientError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = ClientError::config("Invalid token");
assert!(matches!(err, ClientError::Config(_)));
assert_eq!(err.to_string(), "Configuration error: Invalid token");
}
#[test]
fn test_gitlab_api_error() {
let err = ClientError::gitlab_api("Project not found");
assert!(matches!(err, ClientError::GitlabApi { .. }));
assert_eq!(err.to_string(), "GitLab API error: Project not found");
}
#[test]
fn test_retryable_errors() {
assert!(ClientError::Timeout.is_retryable());
assert!(ClientError::rate_limit(None).is_retryable());
assert!(!ClientError::Authentication.is_retryable());
assert!(!ClientError::config("test").is_retryable());
}
#[test]
fn test_network_errors() {
assert!(ClientError::Timeout.is_network_error());
assert!(!ClientError::Authentication.is_network_error());
assert!(!ClientError::config("test").is_network_error());
}
}