lmrc_gitlab/api/
pipelines.rs

1//! Pipeline API operations with builder pattern.
2
3use crate::error::{GitLabError, Result};
4use crate::models::{Pipeline, PipelineBasic, PipelineStatus};
5use gitlab::Gitlab;
6use gitlab::api::{Query, projects::pipelines};
7
8/// Builder for pipeline list operations.
9///
10/// Provides a fluent API for querying pipelines with various filters.
11///
12/// # Examples
13///
14/// ```no_run
15/// use lmrc_gitlab::{GitLabClient, models::PipelineStatus};
16///
17/// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
18/// let client = GitLabClient::new("https://gitlab.com", "token")?;
19///
20/// // List failed pipelines on main branch
21/// let pipelines = client.pipelines("myorg/myproject")
22///     .status(PipelineStatus::Failed)
23///     .ref_name("main")
24///     .limit(10)
25///     .list()
26///     .await?;
27/// # Ok(())
28/// # }
29/// ```
30pub struct PipelineListBuilder<'a> {
31    client: &'a Gitlab,
32    project: String,
33    status: Option<PipelineStatus>,
34    ref_name: Option<String>,
35    limit: Option<usize>,
36}
37
38impl<'a> PipelineListBuilder<'a> {
39    /// Creates a new pipeline list builder.
40    pub(crate) fn new(client: &'a Gitlab, project: impl Into<String>) -> Self {
41        Self {
42            client,
43            project: project.into(),
44            status: None,
45            ref_name: None,
46            limit: None,
47        }
48    }
49
50    /// Filter pipelines by status.
51    ///
52    /// # Examples
53    ///
54    /// ```no_run
55    /// # use lmrc_gitlab::{GitLabClient, models::PipelineStatus};
56    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
57    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
58    /// let pipelines = client.pipelines("myorg/myproject")
59    ///     .status(PipelineStatus::Failed)
60    ///     .list()
61    ///     .await?;
62    /// # Ok(())
63    /// # }
64    /// ```
65    pub fn status(mut self, status: PipelineStatus) -> Self {
66        self.status = Some(status);
67        self
68    }
69
70    /// Filter pipelines by git reference (branch or tag).
71    ///
72    /// # Examples
73    ///
74    /// ```no_run
75    /// # use lmrc_gitlab::GitLabClient;
76    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
77    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
78    /// let pipelines = client.pipelines("myorg/myproject")
79    ///     .ref_name("main")
80    ///     .list()
81    ///     .await?;
82    /// # Ok(())
83    /// # }
84    /// ```
85    pub fn ref_name(mut self, ref_name: impl Into<String>) -> Self {
86        self.ref_name = Some(ref_name.into());
87        self
88    }
89
90    /// Limit the number of pipelines returned.
91    ///
92    /// # Examples
93    ///
94    /// ```no_run
95    /// # use lmrc_gitlab::GitLabClient;
96    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
97    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
98    /// let pipelines = client.pipelines("myorg/myproject")
99    ///     .limit(5)
100    ///     .list()
101    ///     .await?;
102    /// # Ok(())
103    /// # }
104    /// ```
105    pub fn limit(mut self, limit: usize) -> Self {
106        self.limit = Some(limit);
107        self
108    }
109
110    /// Execute the query and return the list of pipelines.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the API request fails or if the project is not found.
115    pub async fn list(self) -> Result<Vec<PipelineBasic>> {
116        let endpoint = pipelines::Pipelines::builder()
117            .project(&self.project)
118            .build()
119            .map_err(|e| GitLabError::api(format!("Failed to build pipeline endpoint: {}", e)))?;
120
121        let mut pipelines: Vec<PipelineBasic> = endpoint
122            .query(self.client)
123            .map_err(|e| GitLabError::api(format!("Failed to query pipelines: {}", e)))?;
124
125        // Apply filters
126        if let Some(status) = self.status {
127            pipelines.retain(|p| p.status == status);
128        }
129
130        if let Some(ref_name) = self.ref_name {
131            pipelines.retain(|p| p.ref_name == ref_name);
132        }
133
134        // Apply limit
135        if let Some(limit) = self.limit {
136            pipelines.truncate(limit);
137        }
138
139        Ok(pipelines)
140    }
141}
142
143/// Builder for single pipeline operations.
144///
145/// Provides operations for a specific pipeline including get, retry, and cancel.
146///
147/// # Examples
148///
149/// ```no_run
150/// use lmrc_gitlab::GitLabClient;
151///
152/// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
153/// let client = GitLabClient::new("https://gitlab.com", "token")?;
154///
155/// // Get pipeline details
156/// let pipeline = client.pipeline("myorg/myproject", 12345)
157///     .get()
158///     .await?;
159///
160/// println!("Pipeline status: {}", pipeline.status);
161/// # Ok(())
162/// # }
163/// ```
164pub struct PipelineBuilder<'a> {
165    client: &'a Gitlab,
166    project: String,
167    pipeline_id: u64,
168}
169
170impl<'a> PipelineBuilder<'a> {
171    /// Creates a new pipeline builder for a specific pipeline.
172    pub(crate) fn new(client: &'a Gitlab, project: impl Into<String>, pipeline_id: u64) -> Self {
173        Self {
174            client,
175            project: project.into(),
176            pipeline_id,
177        }
178    }
179
180    /// Get the pipeline details.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`GitLabError::NotFound`] if the pipeline doesn't exist.
185    ///
186    /// # Examples
187    ///
188    /// ```no_run
189    /// # use lmrc_gitlab::GitLabClient;
190    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
191    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
192    /// let pipeline = client.pipeline("myorg/myproject", 12345)
193    ///     .get()
194    ///     .await?;
195    /// # Ok(())
196    /// # }
197    /// ```
198    pub async fn get(self) -> Result<Pipeline> {
199        let endpoint = pipelines::Pipeline::builder()
200            .project(&self.project)
201            .pipeline(self.pipeline_id)
202            .build()
203            .map_err(|e| GitLabError::api(format!("Failed to build pipeline endpoint: {}", e)))?;
204
205        endpoint.query(self.client).map_err(|e| match e {
206            gitlab::api::ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
207                GitLabError::not_found("pipeline", self.pipeline_id)
208            }
209            _ => GitLabError::api(format!("Failed to get pipeline: {}", e)),
210        })
211    }
212
213    /// Retry the pipeline.
214    ///
215    /// This creates a new pipeline based on the failed jobs of the original pipeline.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the pipeline cannot be retried (e.g., it's still running).
220    ///
221    /// # Examples
222    ///
223    /// ```no_run
224    /// # use lmrc_gitlab::GitLabClient;
225    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
226    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
227    /// let pipeline = client.pipeline("myorg/myproject", 12345)
228    ///     .retry()
229    ///     .await?;
230    /// # Ok(())
231    /// # }
232    /// ```
233    pub async fn retry(self) -> Result<Pipeline> {
234        let endpoint = pipelines::RetryPipeline::builder()
235            .project(&self.project)
236            .pipeline(self.pipeline_id)
237            .build()
238            .map_err(|e| GitLabError::api(format!("Failed to build retry endpoint: {}", e)))?;
239
240        endpoint
241            .query(self.client)
242            .map_err(|e| GitLabError::api(format!("Failed to retry pipeline: {}", e)))
243    }
244
245    /// Cancel the pipeline.
246    ///
247    /// Stops all running jobs in the pipeline.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the pipeline cannot be canceled (e.g., already finished).
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// # use lmrc_gitlab::GitLabClient;
257    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
258    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
259    /// let pipeline = client.pipeline("myorg/myproject", 12345)
260    ///     .cancel()
261    ///     .await?;
262    /// # Ok(())
263    /// # }
264    /// ```
265    pub async fn cancel(self) -> Result<Pipeline> {
266        let endpoint = pipelines::CancelPipeline::builder()
267            .project(&self.project)
268            .pipeline(self.pipeline_id)
269            .build()
270            .map_err(|e| GitLabError::api(format!("Failed to build cancel endpoint: {}", e)))?;
271
272        endpoint
273            .query(self.client)
274            .map_err(|e| GitLabError::api(format!("Failed to cancel pipeline: {}", e)))
275    }
276}
277
278/// High-level pipeline API.
279///
280/// This struct provides the entry point for pipeline operations.
281pub struct PipelineApi;
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_pipeline_list_builder() {
289        // This is a compile-time test to ensure the API is ergonomic
290        fn _compile_test(client: &Gitlab) {
291            let _builder = PipelineListBuilder::new(client, "project")
292                .status(PipelineStatus::Failed)
293                .ref_name("main")
294                .limit(10);
295        }
296    }
297
298    #[test]
299    fn test_pipeline_builder() {
300        // Compile-time test for single pipeline operations
301        fn _compile_test(client: &Gitlab) {
302            let _builder = PipelineBuilder::new(client, "project", 123);
303        }
304    }
305}