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
//! Job API operations with builder pattern.

use crate::error::{GitLabError, Result};
use crate::models::{Job, JobStatus};
use gitlab::Gitlab;
use gitlab::api::{Query, projects::jobs, projects::pipelines::PipelineJobs};

/// Builder for job list operations.
///
/// Can list jobs for a project or for a specific pipeline.
///
/// # Examples
///
/// ```no_run
/// use lmrc_gitlab::{GitLabClient, models::JobStatus};
///
/// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
/// let client = GitLabClient::new("https://gitlab.com", "token")?;
///
/// // List all jobs in a project
/// let jobs = client.jobs("myorg/myproject")
///     .status(JobStatus::Failed)
///     .list()
///     .await?;
///
/// // List jobs in a specific pipeline
/// let pipeline_jobs = client.jobs("myorg/myproject")
///     .pipeline(12345)
///     .list()
///     .await?;
/// # Ok(())
/// # }
/// ```
pub struct JobListBuilder<'a> {
    client: &'a Gitlab,
    project: String,
    pipeline_id: Option<u64>,
    status: Option<JobStatus>,
    limit: Option<usize>,
}

impl<'a> JobListBuilder<'a> {
    /// Creates a new job list builder.
    pub(crate) fn new(client: &'a Gitlab, project: impl Into<String>) -> Self {
        Self {
            client,
            project: project.into(),
            pipeline_id: None,
            status: None,
            limit: None,
        }
    }

    /// Filter jobs by pipeline.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use lmrc_gitlab::GitLabClient;
    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
    /// let jobs = client.jobs("myorg/myproject")
    ///     .pipeline(12345)
    ///     .list()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn pipeline(mut self, pipeline_id: u64) -> Self {
        self.pipeline_id = Some(pipeline_id);
        self
    }

    /// Filter jobs by status.
    pub fn status(mut self, status: JobStatus) -> Self {
        self.status = Some(status);
        self
    }

    /// Limit the number of jobs returned.
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = Some(limit);
        self
    }

    /// Execute the query and return the list of jobs.
    ///
    /// # Errors
    ///
    /// Returns an error if the API request fails.
    pub async fn list(self) -> Result<Vec<Job>> {
        let jobs: Vec<Job> = if let Some(pipeline_id) = self.pipeline_id {
            // List jobs for a specific pipeline
            let endpoint = PipelineJobs::builder()
                .project(&self.project)
                .pipeline(pipeline_id)
                .build()
                .map_err(|e| GitLabError::api(format!("Failed to build jobs endpoint: {}", e)))?;

            endpoint
                .query(self.client)
                .map_err(|e| GitLabError::api(format!("Failed to query jobs: {}", e)))?
        } else {
            // List all jobs in project (requires different endpoint in real implementation)
            // For now, return empty as this would need the Jobs endpoint
            // which would list all jobs across all pipelines
            vec![]
        };

        // Apply filters
        let mut filtered = jobs;
        if let Some(status) = self.status {
            filtered.retain(|j| j.status == status);
        }

        if let Some(limit) = self.limit {
            filtered.truncate(limit);
        }

        Ok(filtered)
    }
}

/// Builder for single job operations.
///
/// Provides operations for a specific job including get, retry, cancel, and logs.
///
/// # Examples
///
/// ```no_run
/// use lmrc_gitlab::GitLabClient;
///
/// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
/// let client = GitLabClient::new("https://gitlab.com", "token")?;
///
/// // Get job details
/// let job = client.job("myorg/myproject", 67890).get().await?;
///
/// // Retry a failed job
/// client.job("myorg/myproject", 67890).retry().await?;
///
/// // Get job logs
/// let logs = client.job("myorg/myproject", 67890).logs().await?;
/// # Ok(())
/// # }
/// ```
pub struct JobBuilder<'a> {
    client: &'a Gitlab,
    project: String,
    job_id: u64,
}

impl<'a> JobBuilder<'a> {
    /// Creates a new job builder for a specific job.
    pub(crate) fn new(client: &'a Gitlab, project: impl Into<String>, job_id: u64) -> Self {
        Self {
            client,
            project: project.into(),
            job_id,
        }
    }

    /// Get the job details.
    ///
    /// # Errors
    ///
    /// Returns [`GitLabError::NotFound`] if the job doesn't exist.
    pub async fn get(self) -> Result<Job> {
        let endpoint = jobs::Job::builder()
            .project(&self.project)
            .job(self.job_id)
            .build()
            .map_err(|e| GitLabError::api(format!("Failed to build job endpoint: {}", e)))?;

        endpoint.query(self.client).map_err(|e| match e {
            gitlab::api::ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
                GitLabError::not_found("job", self.job_id)
            }
            _ => GitLabError::api(format!("Failed to get job: {}", e)),
        })
    }

    /// Retry the job.
    ///
    /// Creates a new job based on the original job.
    ///
    /// # Errors
    ///
    /// Returns an error if the job cannot be retried.
    pub async fn retry(self) -> Result<Job> {
        let endpoint = jobs::RetryJob::builder()
            .project(&self.project)
            .job(self.job_id)
            .build()
            .map_err(|e| GitLabError::api(format!("Failed to build retry endpoint: {}", e)))?;

        endpoint
            .query(self.client)
            .map_err(|e| GitLabError::api(format!("Failed to retry job: {}", e)))
    }

    /// Cancel the job.
    ///
    /// Stops a running or pending job.
    ///
    /// # Errors
    ///
    /// Returns an error if the job cannot be canceled.
    pub async fn cancel(self) -> Result<Job> {
        let endpoint = jobs::CancelJob::builder()
            .project(&self.project)
            .job(self.job_id)
            .build()
            .map_err(|e| GitLabError::api(format!("Failed to build cancel endpoint: {}", e)))?;

        endpoint
            .query(self.client)
            .map_err(|e| GitLabError::api(format!("Failed to cancel job: {}", e)))
    }

    /// Get the job logs (trace).
    ///
    /// # Errors
    ///
    /// Returns an error if logs cannot be retrieved.
    pub async fn logs(self) -> Result<String> {
        let endpoint = jobs::JobTrace::builder()
            .project(&self.project)
            .job(self.job_id)
            .build()
            .map_err(|e| GitLabError::api(format!("Failed to build trace endpoint: {}", e)))?;

        endpoint
            .query(self.client)
            .map_err(|e| GitLabError::api(format!("Failed to get job logs: {}", e)))
    }

    /// Download job artifacts.
    ///
    /// Returns the raw artifact data as bytes.
    ///
    /// # Errors
    ///
    /// Returns an error if artifacts don't exist or cannot be downloaded.
    pub async fn artifacts(self) -> Result<Vec<u8>> {
        // Note: This would require the artifacts endpoint
        // For now, return an error indicating not implemented
        Err(GitLabError::api("Artifact download not yet implemented"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_job_list_builder() {
        fn _compile_test(client: &Gitlab) {
            let _builder = JobListBuilder::new(client, "project")
                .pipeline(123)
                .status(JobStatus::Failed)
                .limit(10);
        }
    }

    #[test]
    fn test_job_builder() {
        fn _compile_test(client: &Gitlab) {
            let _builder = JobBuilder::new(client, "project", 456);
        }
    }
}