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