gh_workflow/
step.rs

1//!
2//! Step-related structures and implementations for GitHub workflow steps.
3
4use 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/// Represents a step in the workflow.
14#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
15#[serde(transparent)]
16pub struct Step<A> {
17    /// The value of the step.
18    pub value: StepValue,
19    #[serde(skip)]
20    pub marker: A,
21}
22
23impl From<Step<Run>> for StepValue {
24    /// Converts a `Step<Run>` into a `StepValue`.
25    fn from(step: Step<Run>) -> Self {
26        step.value
27    }
28}
29
30impl From<Step<Use>> for StepValue {
31    /// Converts a `Step<Use>` into a `StepValue`.
32    fn from(step: Step<Use>) -> Self {
33        step.value
34    }
35}
36
37/// Represents a step that uses an action.
38#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
39pub struct Use;
40
41/// Represents a step that runs a command.
42#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
43pub struct Run;
44
45/// A trait to convert `Step<Run>` and `Step<Use>` to `StepValue`.
46pub trait StepType: Sized + private::Sealed {
47    /// Converts a step to its value representation.
48    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    /// Converts a `Step<Run>` to `StepValue`.
56    fn to_value(s: Step<Self>) -> StepValue {
57        s.into()
58    }
59}
60
61impl StepType for Use {
62    /// Converts a `Step<Use>` to `StepValue`.
63    fn to_value(s: Step<Self>) -> StepValue {
64        s.into()
65    }
66}
67
68/// Represents input parameters for a step.
69#[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    /// Converts an `IndexMap` into an `Input`.
75    fn from(value: IndexMap<String, Value>) -> Self {
76        Self(value)
77    }
78}
79
80impl Merge for Input {
81    /// Merges another `Input` into this one.
82    fn merge(&mut self, other: Self) {
83        self.0.extend(other.0);
84    }
85}
86
87impl Input {
88    /// Adds a new input parameter to the `Input`.
89    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    /// Checks if the `Input` is empty.
95    pub fn is_empty(&self) -> bool {
96        self.0.is_empty()
97    }
98}
99
100/// Represents a step value in the workflow.
101#[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    /// The ID of the step.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    #[merge(strategy = merge::option::overwrite_none)]
114    pub id: Option<String>,
115
116    /// The name of the step.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    #[merge(strategy = merge::option::overwrite_none)]
119    pub name: Option<String>,
120
121    /// The condition under which the step runs.
122    #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
123    #[merge(strategy = merge::option::overwrite_none)]
124    pub if_condition: Option<Expression>,
125
126    /// The action to use in the step.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    #[setters(skip)]
129    #[merge(strategy = merge::option::overwrite_none)]
130    pub uses: Option<String>,
131
132    /// Input parameters for the step.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    #[merge(strategy = merge::option::overwrite_none)]
135    pub with: Option<Input>,
136
137    /// The command to run in the step.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    #[setters(skip)]
140    #[merge(strategy = merge::option::overwrite_none)]
141    pub run: Option<String>,
142
143    /// Shell to run with
144    #[serde(skip_serializing_if = "Option::is_none")]
145    #[merge(strategy = merge::option::overwrite_none)]
146    pub shell: Option<String>,
147
148    /// Environment variables for the step.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    #[merge(strategy = merge::option::overwrite_none)]
151    pub env: Option<Env>,
152
153    /// The timeout for the step in minutes.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    #[merge(strategy = merge::option::overwrite_none)]
156    pub timeout_minutes: Option<u32>,
157
158    /// Whether to continue on error.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    #[merge(strategy = merge::option::overwrite_none)]
161    pub continue_on_error: Option<bool>,
162
163    /// The working directory for the step.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    #[merge(strategy = merge::option::overwrite_none)]
166    pub working_directory: Option<String>,
167
168    /// The retry strategy for the step.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    #[merge(strategy = merge::option::overwrite_none)]
171    pub retry: Option<RetryStrategy>,
172
173    /// Artifacts produced by the step.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    #[merge(strategy = merge::option::overwrite_none)]
176    pub artifacts: Option<Artifacts>,
177}
178
179impl StepValue {
180    /// Creates a new `StepValue` that runs the provided shell command.
181    pub fn run<T: ToString>(cmd: T) -> Self {
182        Self { run: Some(cmd.to_string()), ..Default::default() }
183    }
184
185    /// Creates a new `StepValue` that uses an action.
186    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
203/// Represents a step in the workflow.
204impl<T> Step<T> {
205    /// Adds an environment variable to the step.
206    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
239/// Represents a step that uses an action.
240impl Step<Use> {
241    /// Creates a step pointing to the default GitHub's Checkout Action.
242    pub fn checkout() -> Self {
243        Step::new("Checkout Code").uses("actions", "checkout", "v5")
244    }
245
246    /// Adds a new input to the step.
247    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
260/// Represents a key-value pair for inputs.
261impl<S1: ToString, S2: ToString> From<(S1, S2)> for Input {
262    /// Converts a tuple into an `Input`.
263    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}