lmrc_gitlab/api/
variables.rs

1//! Project CI/CD variables API operations.
2//!
3//! This module provides operations for managing CI/CD variables
4//! at the project level.
5
6use crate::error::{GitLabError, Result};
7use crate::models::{Variable, VariableOptions, VariableType};
8use gitlab::Gitlab;
9use gitlab::api::{ApiError, Query, projects::variables};
10
11/// Builder for managing project CI/CD variables.
12///
13/// Provides operations to create, update, list, and delete
14/// CI/CD variables for a GitLab project.
15///
16/// # Examples
17///
18/// ```no_run
19/// use lmrc_gitlab::{GitLabClient, models::VariableOptions};
20///
21/// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
22/// let client = GitLabClient::new("https://gitlab.com", "token")?;
23///
24/// // Create a new variable
25/// let opts = VariableOptions::new()
26///     .protected(true)
27///     .masked(true);
28///
29/// client.variables("myorg/myproject")
30///     .create("API_KEY", "secret-value", opts)
31///     .await?;
32/// # Ok(())
33/// # }
34/// ```
35pub struct VariableBuilder<'a> {
36    client: &'a Gitlab,
37    project: String,
38}
39
40impl<'a> VariableBuilder<'a> {
41    /// Creates a new variable builder.
42    pub(crate) fn new(client: &'a Gitlab, project: impl Into<String>) -> Self {
43        Self {
44            client,
45            project: project.into(),
46        }
47    }
48
49    /// Create a new CI/CD variable.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the variable already exists or if the request fails.
54    ///
55    /// # Examples
56    ///
57    /// ```no_run
58    /// # use lmrc_gitlab::{GitLabClient, models::VariableOptions};
59    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
60    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
61    /// let opts = VariableOptions::new()
62    ///     .protected(true)
63    ///     .masked(true);
64    ///
65    /// client.variables("myproject")
66    ///     .create("DATABASE_URL", "postgres://...", opts)
67    ///     .await?;
68    /// # Ok(())
69    /// # }
70    /// ```
71    pub async fn create(
72        &self,
73        key: impl AsRef<str>,
74        value: impl AsRef<str>,
75        opts: VariableOptions,
76    ) -> Result<Variable> {
77        let key = key.as_ref();
78        let value = value.as_ref();
79
80        // Build the request using the gitlab crate's API
81        let mut builder = variables::CreateProjectVariable::builder();
82        builder
83            .project(&self.project)
84            .key(key)
85            .value(value)
86            .variable_type(match opts.variable_type {
87                VariableType::EnvVar => variables::ProjectVariableType::EnvVar,
88                VariableType::File => variables::ProjectVariableType::File,
89            })
90            .protected(opts.protected)
91            .masked(opts.masked)
92            .raw(opts.raw);
93
94        if let Some(scope) = opts.environment_scope {
95            builder.environment_scope(scope);
96        }
97
98        if let Some(desc) = opts.description {
99            builder.description(desc);
100        }
101
102        let endpoint = builder.build().map_err(|e| {
103            GitLabError::api(format!("Failed to build create variable request: {}", e))
104        })?;
105
106        endpoint.query(self.client).map_err(|e| match e {
107            ApiError::GitlabService { status, .. } if status.as_u16() == 400 => {
108                GitLabError::conflict(format!("Variable '{}' already exists", key))
109            }
110            _ => GitLabError::api(format!("Failed to create variable '{}': {}", key, e)),
111        })
112    }
113
114    /// Update an existing CI/CD variable.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the variable doesn't exist or if the request fails.
119    ///
120    /// # Examples
121    ///
122    /// ```no_run
123    /// # use lmrc_gitlab::{GitLabClient, models::VariableOptions};
124    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
125    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
126    /// let opts = VariableOptions::new()
127    ///     .protected(true);
128    ///
129    /// client.variables("myproject")
130    ///     .update("API_KEY", "new-secret-value", opts)
131    ///     .await?;
132    /// # Ok(())
133    /// # }
134    /// ```
135    pub async fn update(
136        &self,
137        key: impl AsRef<str>,
138        value: impl AsRef<str>,
139        opts: VariableOptions,
140    ) -> Result<Variable> {
141        let key = key.as_ref();
142        let value = value.as_ref();
143
144        let mut builder = variables::UpdateProjectVariable::builder();
145        builder
146            .project(&self.project)
147            .key(key)
148            .value(value)
149            .variable_type(match opts.variable_type {
150                VariableType::EnvVar => variables::ProjectVariableType::EnvVar,
151                VariableType::File => variables::ProjectVariableType::File,
152            })
153            .protected(opts.protected)
154            .masked(opts.masked)
155            .raw(opts.raw);
156
157        if let Some(scope) = opts.environment_scope {
158            builder.environment_scope(scope);
159        }
160
161        if let Some(desc) = opts.description {
162            builder.description(desc);
163        }
164
165        let endpoint = builder.build().map_err(|e| {
166            GitLabError::api(format!("Failed to build update variable request: {}", e))
167        })?;
168
169        endpoint.query(self.client).map_err(|e| match e {
170            ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
171                GitLabError::not_found("Variable", key)
172            }
173            _ => GitLabError::api(format!("Failed to update variable '{}': {}", key, e)),
174        })
175    }
176
177    /// Create or update a CI/CD variable (upsert).
178    ///
179    /// If the variable exists, it will be updated. Otherwise, it will be created.
180    ///
181    /// # Examples
182    ///
183    /// ```no_run
184    /// # use lmrc_gitlab::{GitLabClient, models::VariableOptions};
185    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
186    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
187    /// let opts = VariableOptions::new()
188    ///     .protected(true)
189    ///     .masked(true);
190    ///
191    /// // Will create if doesn't exist, update if it does
192    /// client.variables("myproject")
193    ///     .set("API_KEY", "secret-value", opts)
194    ///     .await?;
195    /// # Ok(())
196    /// # }
197    /// ```
198    pub async fn set(
199        &self,
200        key: impl AsRef<str>,
201        value: impl AsRef<str>,
202        opts: VariableOptions,
203    ) -> Result<Variable> {
204        let key = key.as_ref();
205
206        // Try to update first
207        match self.update(key, value.as_ref(), opts.clone()).await {
208            Ok(var) => Ok(var),
209            Err(GitLabError::NotFound { .. }) => {
210                // Variable doesn't exist, create it
211                self.create(key, value.as_ref(), opts).await
212            }
213            Err(e) => Err(e),
214        }
215    }
216
217    /// Get a CI/CD variable.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if the variable doesn't exist or if the request fails.
222    ///
223    /// # Examples
224    ///
225    /// ```no_run
226    /// # use lmrc_gitlab::GitLabClient;
227    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
228    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
229    /// let var = client.variables("myproject")
230    ///     .get("API_KEY")
231    ///     .await?;
232    ///
233    /// println!("Value: {}", var.value);
234    /// # Ok(())
235    /// # }
236    /// ```
237    pub async fn get(&self, key: impl AsRef<str>) -> Result<Variable> {
238        let key = key.as_ref();
239
240        let endpoint = variables::ProjectVariable::builder()
241            .project(&self.project)
242            .key(key)
243            .build()
244            .map_err(|e| {
245                GitLabError::api(format!("Failed to build get variable request: {}", e))
246            })?;
247
248        endpoint.query(self.client).map_err(|e| match e {
249            ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
250                GitLabError::not_found("Variable", key)
251            }
252            _ => GitLabError::api(format!("Failed to get variable '{}': {}", key, e)),
253        })
254    }
255
256    /// List all CI/CD variables for the project.
257    ///
258    /// # Examples
259    ///
260    /// ```no_run
261    /// # use lmrc_gitlab::GitLabClient;
262    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
263    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
264    /// let vars = client.variables("myproject")
265    ///     .list()
266    ///     .await?;
267    ///
268    /// for var in vars {
269    ///     println!("{}: {} (protected={})", var.key, var.value, var.protected);
270    /// }
271    /// # Ok(())
272    /// # }
273    /// ```
274    pub async fn list(&self) -> Result<Vec<Variable>> {
275        let endpoint = variables::ProjectVariables::builder()
276            .project(&self.project)
277            .build()
278            .map_err(|e| {
279                GitLabError::api(format!("Failed to build list variables request: {}", e))
280            })?;
281
282        endpoint
283            .query(self.client)
284            .map_err(|e| GitLabError::api(format!("Failed to list variables: {}", e)))
285    }
286
287    /// Delete a CI/CD variable.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the variable doesn't exist or if the request fails.
292    ///
293    /// # Examples
294    ///
295    /// ```no_run
296    /// # use lmrc_gitlab::GitLabClient;
297    /// # async fn example() -> Result<(), lmrc_gitlab::GitLabError> {
298    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
299    /// client.variables("myproject")
300    ///     .delete("OLD_API_KEY")
301    ///     .await?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    pub async fn delete(&self, key: impl AsRef<str>) -> Result<()> {
306        let key = key.as_ref();
307
308        let endpoint = variables::DeleteProjectVariable::builder()
309            .project(&self.project)
310            .key(key)
311            .build()
312            .map_err(|e| {
313                GitLabError::api(format!("Failed to build delete variable request: {}", e))
314            })?;
315
316        // Delete endpoints don't return a value, use ignore() method
317        gitlab::api::ignore(endpoint)
318            .query(self.client)
319            .map_err(|e| match e {
320                ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
321                    GitLabError::not_found("Variable", key)
322                }
323                _ => GitLabError::api(format!("Failed to delete variable '{}': {}", key, e)),
324            })?;
325
326        Ok(())
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_variable_builder() {
336        fn _compile_test(client: &Gitlab) {
337            let _builder = VariableBuilder::new(client, "project");
338        }
339    }
340}