lmrc-gitlab 0.3.16

GitLab API client library for the LMRC Stack - comprehensive Rust library for programmatic control of GitLab via its API
Documentation
//! Error types for the GitLab client library.
//!
//! This module provides a comprehensive error type [`GitLabError`] that covers
//! all possible error scenarios when interacting with the GitLab API.
//!
//! # Examples
//!
//! ```no_run
//! use lmrc_gitlab::{GitLabClient, GitLabError};
//!
//! async fn example() -> Result<(), GitLabError> {
//!     let client = GitLabClient::new("https://gitlab.com", "invalid-token")?;
//!     // This will return GitLabError::Authentication if token is invalid
//!     Ok(())
//! }
//! ```

use std::fmt;

/// Result type alias using [`GitLabError`] as the error type.
pub type Result<T> = std::result::Result<T, GitLabError>;

/// The main error type for all GitLab client operations.
///
/// This enum covers all possible error scenarios including authentication,
/// API errors, network issues, and data validation problems.
#[derive(Debug, thiserror::Error)]
pub enum GitLabError {
    /// Authentication failed due to invalid or missing credentials.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::Authentication("Invalid token".to_string());
    /// assert_eq!(error.to_string(), "Authentication failed: Invalid token");
    /// ```
    #[error("Authentication failed: {0}")]
    Authentication(String),

    /// GitLab API returned an error response.
    ///
    /// This includes HTTP error codes, API-specific errors, and malformed responses.
    #[error("GitLab API error: {0}")]
    Api(String),

    /// HTTP request failed due to network or connection issues.
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),

    /// Requested resource was not found (HTTP 404).
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::NotFound {
    ///     resource: "pipeline".to_string(),
    ///     id: "12345".to_string(),
    /// };
    /// ```
    #[error("Resource not found: {resource} with id {id}")]
    NotFound {
        /// The type of resource (e.g., "pipeline", "job", "project")
        resource: String,
        /// The identifier for the resource
        id: String,
    },

    /// Invalid configuration provided to the client.
    ///
    /// This includes invalid URLs, missing required fields, or incompatible settings.
    #[error("Invalid configuration: {0}")]
    Config(String),

    /// Failed to serialize or deserialize data.
    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    /// Rate limit exceeded for API requests.
    ///
    /// GitLab enforces rate limits on API calls. When exceeded, this error is returned.
    #[error("Rate limit exceeded. Retry after {retry_after:?} seconds")]
    RateLimit {
        /// Number of seconds to wait before retrying (if provided by GitLab)
        retry_after: Option<u64>,
    },

    /// Operation is not permitted due to insufficient permissions.
    #[error("Permission denied: {0}")]
    PermissionDenied(String),

    /// Invalid input or parameters provided.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::InvalidInput {
    ///     field: "status".to_string(),
    ///     message: "Must be one of: success, failed, running".to_string(),
    /// };
    /// ```
    #[error("Invalid input for field '{field}': {message}")]
    InvalidInput {
        /// The field name that has invalid input
        field: String,
        /// Description of why the input is invalid
        message: String,
    },

    /// Operation timed out.
    ///
    /// This can occur when waiting for a pipeline to complete, polling for status, etc.
    #[error("Operation timed out after {seconds} seconds")]
    Timeout {
        /// Number of seconds elapsed before timeout
        seconds: u64,
    },

    /// The underlying GitLab API returned a status that indicates a conflict (HTTP 409).
    ///
    /// Common scenarios include attempting to create a resource that already exists.
    #[error("Conflict: {0}")]
    Conflict(String),

    /// The GitLab server is unavailable or returned an error (HTTP 5xx).
    #[error("GitLab server error: {0}")]
    ServerError(String),

    /// An unexpected or unknown error occurred.
    ///
    /// This is used as a catch-all for errors that don't fit other categories.
    #[error("Unexpected error: {0}")]
    Unexpected(String),
}

impl GitLabError {
    /// Creates an authentication error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::authentication("Token expired");
    /// ```
    pub fn authentication<S: Into<String>>(msg: S) -> Self {
        Self::Authentication(msg.into())
    }

    /// Creates an API error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::api("Invalid project path");
    /// ```
    pub fn api<S: Into<String>>(msg: S) -> Self {
        Self::Api(msg.into())
    }

    /// Creates a not found error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::not_found("pipeline", 12345);
    /// ```
    pub fn not_found<S: Into<String>, I: fmt::Display>(resource: S, id: I) -> Self {
        Self::NotFound {
            resource: resource.into(),
            id: id.to_string(),
        }
    }

    /// Creates a configuration error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::config("Missing API token");
    /// ```
    pub fn config<S: Into<String>>(msg: S) -> Self {
        Self::Config(msg.into())
    }

    /// Creates a rate limit error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::rate_limit(Some(60));
    /// ```
    pub fn rate_limit(retry_after: Option<u64>) -> Self {
        Self::RateLimit { retry_after }
    }

    /// Creates a permission denied error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::permission_denied("Cannot delete protected branch");
    /// ```
    pub fn permission_denied<S: Into<String>>(msg: S) -> Self {
        Self::PermissionDenied(msg.into())
    }

    /// Creates an invalid input error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::invalid_input("ref", "Branch name cannot be empty");
    /// ```
    pub fn invalid_input<S: Into<String>, M: Into<String>>(field: S, message: M) -> Self {
        Self::InvalidInput {
            field: field.into(),
            message: message.into(),
        }
    }

    /// Creates a timeout error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::timeout(300);
    /// ```
    pub fn timeout(seconds: u64) -> Self {
        Self::Timeout { seconds }
    }

    /// Creates a conflict error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::conflict("Pipeline already exists");
    /// ```
    pub fn conflict<S: Into<String>>(msg: S) -> Self {
        Self::Conflict(msg.into())
    }

    /// Creates a server error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::server_error("Internal server error");
    /// ```
    pub fn server_error<S: Into<String>>(msg: S) -> Self {
        Self::ServerError(msg.into())
    }

    /// Creates an unexpected error.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::unexpected("Unknown error occurred");
    /// ```
    pub fn unexpected<S: Into<String>>(msg: S) -> Self {
        Self::Unexpected(msg.into())
    }

    /// Returns `true` if this error is retryable.
    ///
    /// Retryable errors include network issues, rate limits, and server errors.
    ///
    /// # Examples
    ///
    /// ```
    /// # use lmrc_gitlab::GitLabError;
    /// let error = GitLabError::rate_limit(Some(60));
    /// assert!(error.is_retryable());
    ///
    /// let error = GitLabError::not_found("pipeline", 123);
    /// assert!(!error.is_retryable());
    /// ```
    pub fn is_retryable(&self) -> bool {
        matches!(
            self,
            Self::RateLimit { .. } | Self::ServerError(_) | Self::Timeout { .. }
        )
    }

    /// Returns `true` if this is a client error (4xx status codes).
    ///
    /// Client errors indicate problems with the request that cannot be
    /// resolved by retrying.
    pub fn is_client_error(&self) -> bool {
        matches!(
            self,
            Self::Authentication(_)
                | Self::NotFound { .. }
                | Self::PermissionDenied(_)
                | Self::InvalidInput { .. }
                | Self::Conflict(_)
        )
    }

    /// Returns `true` if this is a server error (5xx status codes).
    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());
    }
}