1use derive_setters::Setters;
5use indexmap::IndexMap;
6use merge::Merge;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::toolchain::{Abi, Arch, Component, System, Target, Toolchain, Vendor, Version};
11use crate::{private, Artifacts, Env, Expression, RetryStrategy};
12
13#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
15#[serde(transparent)]
16pub struct Step<A> {
17 pub value: StepValue,
19 #[serde(skip)]
20 pub marker: A,
21}
22
23impl From<Step<Run>> for StepValue {
24 fn from(step: Step<Run>) -> Self {
26 step.value
27 }
28}
29
30impl From<Step<Use>> for StepValue {
31 fn from(step: Step<Use>) -> Self {
33 step.value
34 }
35}
36
37#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
39pub struct Use;
40
41#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
43pub struct Run;
44
45pub trait StepType: Sized + private::Sealed {
47 fn to_value(s: Step<Self>) -> StepValue;
49}
50
51impl private::Sealed for Run {}
52impl private::Sealed for Use {}
53
54impl StepType for Run {
55 fn to_value(s: Step<Self>) -> StepValue {
57 s.into()
58 }
59}
60
61impl StepType for Use {
62 fn to_value(s: Step<Self>) -> StepValue {
64 s.into()
65 }
66}
67
68#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
70#[serde(transparent)]
71pub struct Input(#[serde(skip_serializing_if = "IndexMap::is_empty")] pub IndexMap<String, Value>);
72
73impl From<IndexMap<String, Value>> for Input {
74 fn from(value: IndexMap<String, Value>) -> Self {
76 Self(value)
77 }
78}
79
80impl Merge for Input {
81 fn merge(&mut self, other: Self) {
83 self.0.extend(other.0);
84 }
85}
86
87impl Input {
88 pub fn add<S: ToString, V: Into<Value>>(mut self, key: S, value: V) -> Self {
90 self.0.insert(key.to_string(), value.into());
91 self
92 }
93
94 pub fn is_empty(&self) -> bool {
96 self.0.is_empty()
97 }
98}
99
100#[allow(clippy::duplicated_attributes)]
102#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Merge)]
103#[serde(rename_all = "kebab-case")]
104#[setters(
105 strip_option,
106 into,
107 generate_delegates(ty = "Step<Run>", field = "value"),
108 generate_delegates(ty = "Step<Use>", field = "value")
109)]
110pub struct StepValue {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 #[merge(strategy = merge::option::overwrite_none)]
114 pub id: Option<String>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 #[merge(strategy = merge::option::overwrite_none)]
119 pub name: Option<String>,
120
121 #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
123 #[merge(strategy = merge::option::overwrite_none)]
124 pub if_condition: Option<Expression>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 #[setters(skip)]
129 #[merge(strategy = merge::option::overwrite_none)]
130 pub uses: Option<String>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 #[merge(strategy = merge::option::overwrite_none)]
135 pub with: Option<Input>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 #[setters(skip)]
140 #[merge(strategy = merge::option::overwrite_none)]
141 pub run: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 #[merge(strategy = merge::option::overwrite_none)]
146 pub shell: Option<String>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 #[merge(strategy = merge::option::overwrite_none)]
151 pub env: Option<Env>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
155 #[merge(strategy = merge::option::overwrite_none)]
156 pub timeout_minutes: Option<u32>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 #[merge(strategy = merge::option::overwrite_none)]
161 pub continue_on_error: Option<bool>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 #[merge(strategy = merge::option::overwrite_none)]
166 pub working_directory: Option<String>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 #[merge(strategy = merge::option::overwrite_none)]
171 pub retry: Option<RetryStrategy>,
172
173 #[serde(skip_serializing_if = "Option::is_none")]
175 #[merge(strategy = merge::option::overwrite_none)]
176 pub artifacts: Option<Artifacts>,
177}
178
179impl StepValue {
180 pub fn run<T: ToString>(cmd: T) -> Self {
182 Self { run: Some(cmd.to_string()), ..Default::default() }
183 }
184
185 pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
187 owner: Owner,
188 repo: Repo,
189 version: Version,
190 ) -> Self {
191 Self {
192 uses: Some(format!(
193 "{}/{}@{}",
194 owner.to_string(),
195 repo.to_string(),
196 version.to_string()
197 )),
198 ..Default::default()
199 }
200 }
201}
202
203impl<T> Step<T> {
205 pub fn add_env<R: Into<Env>>(mut self, new_env: R) -> Self {
207 let mut env = self.value.env.take().unwrap_or_default();
208
209 env.0.extend(new_env.into().0);
210 self.value.env = Some(env);
211 self
212 }
213}
214
215impl Step<()> {
216 pub fn new(name: impl ToString) -> Self {
217 Self {
218 value: StepValue::default().name(name.to_string()),
219 marker: Default::default(),
220 }
221 }
222
223 pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
224 mut self,
225 owner: Owner,
226 repo: Repo,
227 version: Version,
228 ) -> Step<Use> {
229 self.value.merge(StepValue::uses(owner, repo, version));
230 Step { value: self.value, marker: Default::default() }
231 }
232
233 pub fn run(mut self, cmd: impl ToString) -> Step<Run> {
234 self.value.merge(StepValue::run(cmd));
235 Step { value: self.value, marker: Default::default() }
236 }
237}
238
239impl Step<Use> {
241 pub fn checkout() -> Self {
243 Step::new("Checkout Code").uses("actions", "checkout", "v5")
244 }
245
246 pub fn add_with<I: Into<Input>>(mut self, new_with: I) -> Self {
248 let mut with = self.value.with.take().unwrap_or_default();
249 with.merge(new_with.into());
250 if with.0.is_empty() {
251 self.value.with = None;
252 } else {
253 self.value.with = Some(with);
254 }
255
256 self
257 }
258}
259
260impl<S1: ToString, S2: ToString> From<(S1, S2)> for Input {
262 fn from(value: (S1, S2)) -> Self {
264 let mut index_map: IndexMap<String, Value> = IndexMap::new();
265 index_map.insert(value.0.to_string(), Value::String(value.1.to_string()));
266 Self(index_map)
267 }
268}
269
270impl Step<Toolchain> {
271 pub fn toolchain() -> Self {
272 Self { value: Default::default(), marker: Toolchain::default() }
273 }
274
275 pub fn add_version(mut self, version: Version) -> Self {
276 self.marker.version.push(version);
277 self
278 }
279
280 pub fn add_component(mut self, component: Component) -> Self {
281 self.marker.components.push(component);
282 self
283 }
284
285 pub fn add_stable(mut self) -> Self {
286 self.marker.version.push(Version::Stable);
287 self
288 }
289
290 pub fn add_nightly(mut self) -> Self {
291 self.marker.version.push(Version::Nightly);
292 self
293 }
294
295 pub fn add_clippy(mut self) -> Self {
296 self.marker.components.push(Component::Clippy);
297 self
298 }
299
300 pub fn add_fmt(mut self) -> Self {
301 self.marker.components.push(Component::Rustfmt);
302 self
303 }
304
305 pub fn target(mut self, arch: Arch, vendor: Vendor, system: System, abi: Option<Abi>) -> Self {
306 self.marker.target = Some(Target { arch, vendor, system, abi });
307 self
308 }
309}
310
311impl StepType for Toolchain {
312 fn to_value(s: Step<Self>) -> StepValue {
313 let step: Step<Use> = s.marker.into();
314 StepValue::from(step)
315 }
316}