Skip to main content

wrkflw_models/
lib.rs

1pub struct ValidationResult {
2    pub is_valid: bool,
3    pub issues: Vec<String>,
4}
5
6impl Default for ValidationResult {
7    fn default() -> Self {
8        Self::new()
9    }
10}
11
12impl ValidationResult {
13    pub fn new() -> Self {
14        ValidationResult {
15            is_valid: true,
16            issues: Vec::new(),
17        }
18    }
19
20    pub fn add_issue(&mut self, issue: String) {
21        self.is_valid = false;
22        self.issues.push(issue);
23    }
24}
25
26// GitLab pipeline models
27pub mod gitlab {
28    use serde::{Deserialize, Deserializer, Serialize};
29    use std::collections::HashMap;
30
31    fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
32    where
33        D: Deserializer<'de>,
34    {
35        #[derive(Deserialize)]
36        #[serde(untagged)]
37        enum StringOrVec {
38            String(String),
39            Vec(Vec<String>),
40        }
41        let value = Option::<StringOrVec>::deserialize(deserializer)?;
42        match value {
43            Some(StringOrVec::String(s)) => Ok(Some(vec![s])),
44            Some(StringOrVec::Vec(v)) => Ok(Some(v)),
45            None => Ok(None),
46        }
47    }
48
49    fn deserialize_variables<'de, D>(
50        deserializer: D,
51    ) -> Result<Option<HashMap<String, String>>, D::Error>
52    where
53        D: Deserializer<'de>,
54    {
55        let opt: Option<HashMap<String, serde_yaml::Value>> = Option::deserialize(deserializer)?;
56        Ok(opt.map(|m| {
57            m.into_iter()
58                .map(|(k, v)| {
59                    let s = match v {
60                        serde_yaml::Value::String(s) => s,
61                        serde_yaml::Value::Number(n) => n.to_string(),
62                        serde_yaml::Value::Bool(b) => b.to_string(),
63                        serde_yaml::Value::Null => String::new(),
64                        other => serde_yaml::to_string(&other)
65                            .map(|s| s.trim().to_string())
66                            .unwrap_or_default(),
67                    };
68                    (k, s)
69                })
70                .collect()
71        }))
72    }
73
74    /// Represents a GitLab CI/CD pipeline configuration
75    #[derive(Debug, Serialize, Deserialize, Clone)]
76    pub struct Pipeline {
77        /// Default image for all jobs
78        #[serde(skip_serializing_if = "Option::is_none")]
79        pub image: Option<Image>,
80
81        /// Global variables available to all jobs
82        #[serde(
83            default,
84            skip_serializing_if = "Option::is_none",
85            deserialize_with = "deserialize_variables"
86        )]
87        pub variables: Option<HashMap<String, String>>,
88
89        /// Pipeline stages in execution order
90        #[serde(skip_serializing_if = "Option::is_none")]
91        pub stages: Option<Vec<String>>,
92
93        /// Default before_script for all jobs
94        #[serde(
95            default,
96            skip_serializing_if = "Option::is_none",
97            deserialize_with = "deserialize_string_or_vec"
98        )]
99        pub before_script: Option<Vec<String>>,
100
101        /// Default after_script for all jobs
102        #[serde(
103            default,
104            skip_serializing_if = "Option::is_none",
105            deserialize_with = "deserialize_string_or_vec"
106        )]
107        pub after_script: Option<Vec<String>>,
108
109        /// Default settings for all jobs
110        #[serde(skip_serializing_if = "Option::is_none")]
111        pub default: Option<serde_yaml::Value>,
112
113        /// Job definitions (name => job)
114        #[serde(flatten)]
115        pub jobs: HashMap<String, Job>,
116
117        /// Workflow rules for the pipeline
118        #[serde(skip_serializing_if = "Option::is_none")]
119        pub workflow: Option<Workflow>,
120
121        /// Includes for pipeline configuration
122        #[serde(skip_serializing_if = "Option::is_none")]
123        pub include: Option<Vec<Include>>,
124    }
125
126    /// A job in a GitLab CI/CD pipeline
127    #[derive(Debug, Serialize, Deserialize, Clone)]
128    pub struct Job {
129        /// The stage this job belongs to
130        #[serde(skip_serializing_if = "Option::is_none")]
131        pub stage: Option<String>,
132
133        /// Docker image to use for this job
134        #[serde(skip_serializing_if = "Option::is_none")]
135        pub image: Option<Image>,
136
137        /// Script commands to run
138        #[serde(
139            default,
140            skip_serializing_if = "Option::is_none",
141            deserialize_with = "deserialize_string_or_vec"
142        )]
143        pub script: Option<Vec<String>>,
144
145        /// Commands to run before the main script
146        #[serde(
147            default,
148            skip_serializing_if = "Option::is_none",
149            deserialize_with = "deserialize_string_or_vec"
150        )]
151        pub before_script: Option<Vec<String>>,
152
153        /// Commands to run after the main script
154        #[serde(
155            default,
156            skip_serializing_if = "Option::is_none",
157            deserialize_with = "deserialize_string_or_vec"
158        )]
159        pub after_script: Option<Vec<String>>,
160
161        /// When to run the job (on_success, on_failure, always, manual)
162        #[serde(skip_serializing_if = "Option::is_none")]
163        pub when: Option<String>,
164
165        /// Allow job failure
166        #[serde(skip_serializing_if = "Option::is_none")]
167        pub allow_failure: Option<bool>,
168
169        /// Services to run alongside the job
170        #[serde(skip_serializing_if = "Option::is_none")]
171        pub services: Option<Vec<Service>>,
172
173        /// Tags to define which runners can execute this job
174        #[serde(skip_serializing_if = "Option::is_none")]
175        pub tags: Option<Vec<String>>,
176
177        /// Job-specific variables
178        #[serde(
179            default,
180            skip_serializing_if = "Option::is_none",
181            deserialize_with = "deserialize_variables"
182        )]
183        pub variables: Option<HashMap<String, String>>,
184
185        /// Job dependencies
186        #[serde(skip_serializing_if = "Option::is_none")]
187        pub dependencies: Option<Vec<String>>,
188
189        /// Artifacts to store after job execution
190        #[serde(skip_serializing_if = "Option::is_none")]
191        pub artifacts: Option<Artifacts>,
192
193        /// Cache configuration
194        #[serde(skip_serializing_if = "Option::is_none")]
195        pub cache: Option<Cache>,
196
197        /// Rules for when this job should run
198        #[serde(skip_serializing_if = "Option::is_none")]
199        pub rules: Option<Vec<Rule>>,
200
201        /// Only run on specified refs
202        #[serde(skip_serializing_if = "Option::is_none")]
203        pub only: Option<Only>,
204
205        /// Exclude specified refs
206        #[serde(skip_serializing_if = "Option::is_none")]
207        pub except: Option<Except>,
208
209        /// Retry configuration
210        #[serde(skip_serializing_if = "Option::is_none")]
211        pub retry: Option<Retry>,
212
213        /// Timeout for the job in seconds
214        #[serde(skip_serializing_if = "Option::is_none")]
215        pub timeout: Option<String>,
216
217        /// Mark job as parallel and specify instance count
218        #[serde(skip_serializing_if = "Option::is_none")]
219        pub parallel: Option<usize>,
220
221        /// Flag to indicate this is a template job
222        #[serde(skip_serializing_if = "Option::is_none")]
223        pub template: Option<bool>,
224
225        /// List of jobs this job extends from
226        #[serde(
227            default,
228            skip_serializing_if = "Option::is_none",
229            deserialize_with = "deserialize_string_or_vec"
230        )]
231        pub extends: Option<Vec<String>>,
232
233        /// Job needs (dependencies with more granular control)
234        #[serde(skip_serializing_if = "Option::is_none")]
235        pub needs: Option<serde_yaml::Value>,
236
237        /// Whether the job can be interrupted by a newer pipeline
238        #[serde(skip_serializing_if = "Option::is_none")]
239        pub interruptible: Option<bool>,
240    }
241
242    /// Docker image configuration
243    #[derive(Debug, Serialize, Deserialize, Clone)]
244    #[serde(untagged)]
245    pub enum Image {
246        /// Simple image name as string
247        Simple(String),
248        /// Detailed image configuration
249        Detailed {
250            /// Image name
251            name: String,
252            /// Entrypoint to override in the image
253            #[serde(skip_serializing_if = "Option::is_none")]
254            entrypoint: Option<Vec<String>>,
255        },
256    }
257
258    /// Service container to run alongside a job
259    #[derive(Debug, Serialize, Deserialize, Clone)]
260    #[serde(untagged)]
261    pub enum Service {
262        /// Simple service name as string
263        Simple(String),
264        /// Detailed service configuration
265        Detailed {
266            /// Service name/image
267            name: String,
268            /// Command to run in the service container
269            #[serde(skip_serializing_if = "Option::is_none")]
270            command: Option<Vec<String>>,
271            /// Entrypoint to override in the image
272            #[serde(skip_serializing_if = "Option::is_none")]
273            entrypoint: Option<Vec<String>>,
274        },
275    }
276
277    /// Artifacts configuration
278    #[derive(Debug, Serialize, Deserialize, Clone)]
279    pub struct Artifacts {
280        /// Paths to include as artifacts
281        #[serde(skip_serializing_if = "Option::is_none")]
282        pub paths: Option<Vec<String>>,
283        /// Artifact expiration duration
284        #[serde(skip_serializing_if = "Option::is_none")]
285        pub expire_in: Option<String>,
286        /// When to upload artifacts (on_success, on_failure, always)
287        #[serde(skip_serializing_if = "Option::is_none")]
288        pub when: Option<String>,
289        /// Reports configuration (e.g., junit, coverage_report)
290        #[serde(skip_serializing_if = "Option::is_none")]
291        pub reports: Option<serde_yaml::Value>,
292    }
293
294    /// Cache key configuration
295    #[derive(Debug, Serialize, Deserialize, Clone)]
296    #[serde(untagged)]
297    pub enum CacheKey {
298        /// Simple string key
299        Simple(String),
300        /// Structured key with files and optional prefix
301        Structured {
302            /// Files to use for cache key generation
303            files: Vec<String>,
304            /// Optional prefix for the cache key
305            #[serde(skip_serializing_if = "Option::is_none")]
306            prefix: Option<String>,
307        },
308    }
309
310    /// Cache configuration
311    #[derive(Debug, Serialize, Deserialize, Clone)]
312    pub struct Cache {
313        /// Cache key
314        #[serde(skip_serializing_if = "Option::is_none")]
315        pub key: Option<CacheKey>,
316        /// Paths to cache
317        #[serde(skip_serializing_if = "Option::is_none")]
318        pub paths: Option<Vec<String>>,
319        /// When to save cache (on_success, on_failure, always)
320        #[serde(skip_serializing_if = "Option::is_none")]
321        pub when: Option<String>,
322        /// Cache policy
323        #[serde(skip_serializing_if = "Option::is_none")]
324        pub policy: Option<String>,
325    }
326
327    /// Rule for conditional job execution
328    #[derive(Debug, Serialize, Deserialize, Clone)]
329    pub struct Rule {
330        /// If condition expression
331        #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
332        pub if_: Option<String>,
333        /// When to run if condition is true
334        #[serde(skip_serializing_if = "Option::is_none")]
335        pub when: Option<String>,
336        /// Variables to set if condition is true
337        #[serde(skip_serializing_if = "Option::is_none")]
338        pub variables: Option<HashMap<String, String>>,
339    }
340
341    /// Only/except configuration
342    #[derive(Debug, Serialize, Deserialize, Clone)]
343    #[serde(untagged)]
344    pub enum Only {
345        /// Simple list of refs
346        Refs(Vec<String>),
347        /// Detailed configuration
348        Complex {
349            /// Refs to include
350            #[serde(skip_serializing_if = "Option::is_none")]
351            refs: Option<Vec<String>>,
352            /// Branch patterns to include
353            #[serde(skip_serializing_if = "Option::is_none")]
354            branches: Option<Vec<String>>,
355            /// Tags to include
356            #[serde(skip_serializing_if = "Option::is_none")]
357            tags: Option<Vec<String>>,
358            /// Pipeline types to include
359            #[serde(skip_serializing_if = "Option::is_none")]
360            variables: Option<Vec<String>>,
361            /// Changes to files that trigger the job
362            #[serde(skip_serializing_if = "Option::is_none")]
363            changes: Option<Vec<String>>,
364        },
365    }
366
367    /// Except configuration
368    #[derive(Debug, Serialize, Deserialize, Clone)]
369    #[serde(untagged)]
370    pub enum Except {
371        /// Simple list of refs
372        Refs(Vec<String>),
373        /// Detailed configuration
374        Complex {
375            /// Refs to exclude
376            #[serde(skip_serializing_if = "Option::is_none")]
377            refs: Option<Vec<String>>,
378            /// Branch patterns to exclude
379            #[serde(skip_serializing_if = "Option::is_none")]
380            branches: Option<Vec<String>>,
381            /// Tags to exclude
382            #[serde(skip_serializing_if = "Option::is_none")]
383            tags: Option<Vec<String>>,
384            /// Pipeline types to exclude
385            #[serde(skip_serializing_if = "Option::is_none")]
386            variables: Option<Vec<String>>,
387            /// Changes to files that don't trigger the job
388            #[serde(skip_serializing_if = "Option::is_none")]
389            changes: Option<Vec<String>>,
390        },
391    }
392
393    /// Workflow configuration
394    #[derive(Debug, Serialize, Deserialize, Clone)]
395    pub struct Workflow {
396        /// Rules for when to run the pipeline
397        pub rules: Vec<Rule>,
398    }
399
400    /// Retry configuration
401    #[derive(Debug, Serialize, Deserialize, Clone)]
402    #[serde(untagged)]
403    pub enum Retry {
404        /// Simple max attempts
405        MaxAttempts(u32),
406        /// Detailed retry configuration
407        Detailed {
408            /// Maximum retry attempts
409            max: u32,
410            /// When to retry
411            #[serde(skip_serializing_if = "Option::is_none")]
412            when: Option<Vec<String>>,
413        },
414    }
415
416    /// Include configuration for external pipeline files
417    #[derive(Debug, Serialize, Deserialize, Clone)]
418    #[serde(untagged)]
419    pub enum Include {
420        /// Simple string include
421        Local(String),
422        /// Detailed include configuration
423        Detailed {
424            /// Local file path
425            #[serde(skip_serializing_if = "Option::is_none")]
426            local: Option<String>,
427            /// Remote file URL
428            #[serde(skip_serializing_if = "Option::is_none")]
429            remote: Option<String>,
430            /// Include from project
431            #[serde(skip_serializing_if = "Option::is_none")]
432            project: Option<String>,
433            /// Include specific file from project
434            #[serde(skip_serializing_if = "Option::is_none")]
435            file: Option<String>,
436            /// Include template
437            #[serde(skip_serializing_if = "Option::is_none")]
438            template: Option<String>,
439            /// Ref to use when including from project
440            #[serde(skip_serializing_if = "Option::is_none")]
441            ref_: Option<String>,
442        },
443    }
444}