lmrc_gitlab/models/
job.rs

1//! Job-related data models.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// A CI/CD job within a pipeline.
7///
8/// Jobs are individual units of work in a pipeline, such as building,
9/// testing, or deploying code.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct Job {
12    /// Unique job identifier
13    pub id: u64,
14
15    /// Job name as defined in .gitlab-ci.yml
16    pub name: String,
17
18    /// Current status of the job
19    pub status: JobStatus,
20
21    /// Pipeline stage the job belongs to
22    pub stage: String,
23
24    /// Git reference (branch or tag) the job ran on
25    #[serde(rename = "ref")]
26    pub ref_name: String,
27
28    /// Git commit SHA the job ran on
29    #[serde(default)]
30    pub sha: Option<String>,
31
32    /// When the job was created
33    pub created_at: DateTime<Utc>,
34
35    /// When the job started running
36    pub started_at: Option<DateTime<Utc>>,
37
38    /// When the job finished
39    pub finished_at: Option<DateTime<Utc>>,
40
41    /// Duration of job execution in seconds
42    pub duration: Option<f64>,
43
44    /// Time spent queued before execution in seconds
45    pub queued_duration: Option<f64>,
46
47    /// Web URL to view the job in GitLab UI
48    pub web_url: String,
49
50    /// User who triggered the job
51    #[serde(default)]
52    pub user: Option<JobUser>,
53
54    /// Runner that executed the job
55    #[serde(default)]
56    pub runner: Option<JobRunner>,
57
58    /// Whether the job can be retried
59    #[serde(default)]
60    pub allow_failure: bool,
61
62    /// Tag list for the job
63    #[serde(default)]
64    pub tag_list: Vec<String>,
65
66    /// Artifacts associated with this job
67    #[serde(default)]
68    pub artifacts: Vec<Artifact>,
69
70    /// Coverage percentage for this job
71    #[serde(default)]
72    pub coverage: Option<f64>,
73}
74
75/// User information associated with a job.
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct JobUser {
78    /// User ID
79    pub id: u64,
80
81    /// Username
82    pub username: String,
83
84    /// Display name
85    pub name: String,
86
87    /// User state
88    pub state: String,
89
90    /// URL to user's avatar
91    pub avatar_url: Option<String>,
92
93    /// Web URL to user's profile
94    pub web_url: String,
95}
96
97/// Runner information for a job.
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99pub struct JobRunner {
100    /// Runner ID
101    pub id: u64,
102
103    /// Runner description
104    pub description: String,
105
106    /// Whether the runner is active
107    #[serde(default)]
108    pub active: bool,
109
110    /// Whether the runner is shared
111    #[serde(default)]
112    pub is_shared: bool,
113
114    /// Runner name
115    #[serde(default)]
116    pub name: Option<String>,
117}
118
119/// Artifact produced by a job.
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct Artifact {
122    /// Artifact filename
123    pub filename: String,
124
125    /// Artifact size in bytes
126    pub size: u64,
127
128    /// Artifact file type
129    #[serde(default)]
130    pub file_type: Option<String>,
131
132    /// Artifact file format
133    #[serde(default)]
134    pub file_format: Option<String>,
135}
136
137/// Status of a CI/CD job.
138///
139/// Represents the various states a GitLab job can be in during its lifecycle.
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
141#[serde(rename_all = "snake_case")]
142pub enum JobStatus {
143    /// Job created but not yet running
144    Created,
145
146    /// Job is waiting to be picked up by a runner
147    Pending,
148
149    /// Job is currently running
150    Running,
151
152    /// Job completed successfully
153    Success,
154
155    /// Job failed
156    Failed,
157
158    /// Job was canceled
159    Canceled,
160
161    /// Job was skipped
162    Skipped,
163
164    /// Job is waiting for manual action
165    Manual,
166
167    /// Job is scheduled to run
168    Scheduled,
169}
170
171impl JobStatus {
172    /// Returns `true` if the job is in a terminal state (completed, won't change).
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use lmrc_gitlab::models::JobStatus;
178    ///
179    /// assert!(JobStatus::Success.is_finished());
180    /// assert!(JobStatus::Failed.is_finished());
181    /// assert!(!JobStatus::Running.is_finished());
182    /// ```
183    pub fn is_finished(self) -> bool {
184        matches!(
185            self,
186            Self::Success | Self::Failed | Self::Canceled | Self::Skipped
187        )
188    }
189
190    /// Returns `true` if the job is currently active (running or pending).
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use lmrc_gitlab::models::JobStatus;
196    ///
197    /// assert!(JobStatus::Running.is_active());
198    /// assert!(JobStatus::Pending.is_active());
199    /// assert!(!JobStatus::Success.is_active());
200    /// ```
201    pub fn is_active(self) -> bool {
202        matches!(self, Self::Created | Self::Pending | Self::Running)
203    }
204
205    /// Returns `true` if the job succeeded.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use lmrc_gitlab::models::JobStatus;
211    ///
212    /// assert!(JobStatus::Success.is_successful());
213    /// assert!(!JobStatus::Failed.is_successful());
214    /// ```
215    pub fn is_successful(self) -> bool {
216        self == Self::Success
217    }
218
219    /// Returns `true` if the job failed.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use lmrc_gitlab::models::JobStatus;
225    ///
226    /// assert!(JobStatus::Failed.is_failed());
227    /// assert!(!JobStatus::Success.is_failed());
228    /// ```
229    pub fn is_failed(self) -> bool {
230        self == Self::Failed
231    }
232}
233
234impl std::fmt::Display for JobStatus {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::Created => write!(f, "created"),
238            Self::Pending => write!(f, "pending"),
239            Self::Running => write!(f, "running"),
240            Self::Success => write!(f, "success"),
241            Self::Failed => write!(f, "failed"),
242            Self::Canceled => write!(f, "canceled"),
243            Self::Skipped => write!(f, "skipped"),
244            Self::Manual => write!(f, "manual"),
245            Self::Scheduled => write!(f, "scheduled"),
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_job_status_is_finished() {
256        assert!(JobStatus::Success.is_finished());
257        assert!(JobStatus::Failed.is_finished());
258        assert!(JobStatus::Canceled.is_finished());
259        assert!(JobStatus::Skipped.is_finished());
260
261        assert!(!JobStatus::Running.is_finished());
262        assert!(!JobStatus::Pending.is_finished());
263    }
264
265    #[test]
266    fn test_job_status_is_active() {
267        assert!(JobStatus::Running.is_active());
268        assert!(JobStatus::Pending.is_active());
269        assert!(JobStatus::Created.is_active());
270
271        assert!(!JobStatus::Success.is_active());
272        assert!(!JobStatus::Failed.is_active());
273    }
274
275    #[test]
276    fn test_job_status_display() {
277        assert_eq!(JobStatus::Success.to_string(), "success");
278        assert_eq!(JobStatus::Failed.to_string(), "failed");
279        assert_eq!(JobStatus::Running.to_string(), "running");
280    }
281
282    #[test]
283    fn test_job_status_serialization() {
284        let status = JobStatus::Success;
285        let json = serde_json::to_string(&status).unwrap();
286        assert_eq!(json, "\"success\"");
287
288        let deserialized: JobStatus = serde_json::from_str(&json).unwrap();
289        assert_eq!(deserialized, JobStatus::Success);
290    }
291}