1use derive_setters::Setters;
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::concurrency::Concurrency;
10use crate::step::{Step, StepType, StepValue};
11use crate::{
12 Artifacts, Container, Defaults, Env, Expression, Permissions, RetryStrategy, Secret, Strategy,
13};
14
15#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
17#[serde(transparent)]
18pub struct RunsOn(Value);
19
20impl<T> From<T> for RunsOn
21where
22 T: Into<Value>,
23{
24 fn from(value: T) -> Self {
26 Self(value.into())
27 }
28}
29
30#[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq)]
33#[serde(rename_all = "kebab-case")]
34#[setters(strip_option, into)]
35pub struct Job {
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub needs: Option<Vec<String>>,
38 #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
39 pub cond: Option<Expression>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub name: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub runs_on: Option<RunsOn>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub permissions: Option<Permissions>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub environment: Option<crate::Environment>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub concurrency: Option<Concurrency>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub outputs: Option<IndexMap<String, String>>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub env: Option<Env>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub defaults: Option<Defaults>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub timeout_minutes: Option<u32>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub continue_on_error: Option<bool>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub container: Option<Container>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub services: Option<IndexMap<String, Container>>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub strategy: Option<Strategy>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub steps: Option<Vec<StepValue>>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub uses: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub secrets: Option<IndexMap<String, Secret>>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub retry: Option<RetryStrategy>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub artifacts: Option<Artifacts>,
76}
77
78impl Default for Job {
79 fn default() -> Self {
81 Self {
82 needs: None,
83 cond: None,
84 name: None,
85 runs_on: Some(RunsOn(Value::from("ubuntu-latest"))),
86 permissions: None,
87 environment: None,
88 concurrency: None,
89 outputs: None,
90 env: None,
91 defaults: None,
92 timeout_minutes: None,
93 continue_on_error: None,
94 container: None,
95 services: None,
96 strategy: None,
97 steps: None,
98 uses: None,
99 secrets: None,
100 retry: None,
101 artifacts: None,
102 }
103 }
104}
105
106impl Job {
107 pub fn new<T: ToString>(name: T) -> Self {
109 Self {
110 name: Some(name.to_string()),
111 runs_on: Some(RunsOn(Value::from("ubuntu-latest"))),
112 ..Default::default()
113 }
114 }
115
116 pub fn add_step<S: Into<Step<T>>, T: StepType>(mut self, step: S) -> Self {
118 let mut steps = self.steps.take().unwrap_or_default();
119 let step: Step<T> = step.into();
120 let step: StepValue = T::to_value(step);
121 steps.push(step);
122 self.steps = Some(steps);
123 self
124 }
125
126 pub fn add_env<T: Into<Env>>(mut self, new_env: T) -> Self {
128 let mut env = self.env.take().unwrap_or_default();
129
130 env.0.extend(new_env.into().0);
131 self.env = Some(env);
132 self
133 }
134
135 pub fn add_needs<J: ToString>(mut self, job_id: J) -> Self {
136 if let Some(needs) = self.needs.as_mut() {
137 needs.push(job_id.to_string());
138 } else {
139 self.needs = Some(vec![job_id.to_string()]);
140 }
141 self
142 }
143
144 pub fn add_output<K: ToString, V: ToString>(mut self, key: K, value: V) -> Self {
146 let mut outputs = self.outputs.take().unwrap_or_default();
147 outputs.insert(key.to_string(), value.to_string());
148 self.outputs = Some(outputs);
149 self
150 }
151
152 pub fn add_service<K: ToString, V: Into<Container>>(mut self, key: K, service: V) -> Self {
154 let mut services = self.services.take().unwrap_or_default();
155 services.insert(key.to_string(), service.into());
156 self.services = Some(services);
157 self
158 }
159
160 pub fn add_secret<K: ToString, V: Into<Secret>>(mut self, key: K, secret: V) -> Self {
162 let mut secrets = self.secrets.take().unwrap_or_default();
163 secrets.insert(key.to_string(), secret.into());
164 self.secrets = Some(secrets);
165 self
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_job_default_sets_runs_on() {
175 let job = Job::default();
176 assert!(job.runs_on.is_some());
177
178 if let Some(runs_on) = job.runs_on {
180 assert_eq!(
181 runs_on.0,
182 serde_json::Value::String("ubuntu-latest".to_string())
183 );
184 }
185 }
186}