use std::fmt;
pub type Result<T> = std::result::Result<T, GitLabError>;
#[derive(Debug, thiserror::Error)]
pub enum GitLabError {
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("GitLab API error: {0}")]
Api(String),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Resource not found: {resource} with id {id}")]
NotFound {
resource: String,
id: String,
},
#[error("Invalid configuration: {0}")]
Config(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Rate limit exceeded. Retry after {retry_after:?} seconds")]
RateLimit {
retry_after: Option<u64>,
},
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Invalid input for field '{field}': {message}")]
InvalidInput {
field: String,
message: String,
},
#[error("Operation timed out after {seconds} seconds")]
Timeout {
seconds: u64,
},
#[error("Conflict: {0}")]
Conflict(String),
#[error("GitLab server error: {0}")]
ServerError(String),
#[error("Unexpected error: {0}")]
Unexpected(String),
}
impl GitLabError {
pub fn authentication<S: Into<String>>(msg: S) -> Self {
Self::Authentication(msg.into())
}
pub fn api<S: Into<String>>(msg: S) -> Self {
Self::Api(msg.into())
}
pub fn not_found<S: Into<String>, I: fmt::Display>(resource: S, id: I) -> Self {
Self::NotFound {
resource: resource.into(),
id: id.to_string(),
}
}
pub fn config<S: Into<String>>(msg: S) -> Self {
Self::Config(msg.into())
}
pub fn rate_limit(retry_after: Option<u64>) -> Self {
Self::RateLimit { retry_after }
}
pub fn permission_denied<S: Into<String>>(msg: S) -> Self {
Self::PermissionDenied(msg.into())
}
pub fn invalid_input<S: Into<String>, M: Into<String>>(field: S, message: M) -> Self {
Self::InvalidInput {
field: field.into(),
message: message.into(),
}
}
pub fn timeout(seconds: u64) -> Self {
Self::Timeout { seconds }
}
pub fn conflict<S: Into<String>>(msg: S) -> Self {
Self::Conflict(msg.into())
}
pub fn server_error<S: Into<String>>(msg: S) -> Self {
Self::ServerError(msg.into())
}
pub fn unexpected<S: Into<String>>(msg: S) -> Self {
Self::Unexpected(msg.into())
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::RateLimit { .. } | Self::ServerError(_) | Self::Timeout { .. }
)
}
pub fn is_client_error(&self) -> bool {
matches!(
self,
Self::Authentication(_)
| Self::NotFound { .. }
| Self::PermissionDenied(_)
| Self::InvalidInput { .. }
| Self::Conflict(_)
)
}
pub fn is_server_error(&self) -> bool {
matches!(self, Self::ServerError(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let error = GitLabError::authentication("Invalid token");
assert_eq!(error.to_string(), "Authentication failed: Invalid token");
let error = GitLabError::not_found("pipeline", 12345);
assert_eq!(
error.to_string(),
"Resource not found: pipeline with id 12345"
);
}
#[test]
fn test_is_retryable() {
assert!(GitLabError::rate_limit(Some(60)).is_retryable());
assert!(GitLabError::server_error("Error").is_retryable());
assert!(GitLabError::timeout(300).is_retryable());
assert!(!GitLabError::not_found("job", 123).is_retryable());
assert!(!GitLabError::authentication("Invalid").is_retryable());
}
#[test]
fn test_is_client_error() {
assert!(GitLabError::authentication("Invalid").is_client_error());
assert!(GitLabError::not_found("job", 123).is_client_error());
assert!(GitLabError::permission_denied("Denied").is_client_error());
assert!(!GitLabError::server_error("Error").is_client_error());
assert!(!GitLabError::rate_limit(None).is_client_error());
}
#[test]
fn test_is_server_error() {
assert!(GitLabError::server_error("Error").is_server_error());
assert!(!GitLabError::not_found("job", 123).is_server_error());
assert!(!GitLabError::authentication("Invalid").is_server_error());
}
}