use derive_setters::Setters;
use indexmap::IndexMap;
use merge::Merge;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::toolchain::{Abi, Arch, Component, System, Target, Toolchain, Vendor, Version};
use crate::{private, Artifacts, Env, Expression, RetryStrategy};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(transparent)]
pub struct Step<A> {
pub value: StepValue,
#[serde(skip)]
pub marker: A,
}
impl From<Step<Run>> for StepValue {
fn from(step: Step<Run>) -> Self {
step.value
}
}
impl From<Step<Use>> for StepValue {
fn from(step: Step<Use>) -> Self {
step.value
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Use;
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Run;
pub trait StepType: Sized + private::Sealed {
fn to_value(s: Step<Self>) -> StepValue;
}
impl private::Sealed for Run {}
impl private::Sealed for Use {}
impl StepType for Run {
fn to_value(s: Step<Self>) -> StepValue {
s.into()
}
}
impl StepType for Use {
fn to_value(s: Step<Self>) -> StepValue {
s.into()
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(transparent)]
pub struct Input(#[serde(skip_serializing_if = "IndexMap::is_empty")] pub IndexMap<String, Value>);
impl From<IndexMap<String, Value>> for Input {
fn from(value: IndexMap<String, Value>) -> Self {
Self(value)
}
}
impl Merge for Input {
fn merge(&mut self, other: Self) {
self.0.extend(other.0);
}
}
impl Input {
pub fn add<S: ToString, V: Into<Value>>(mut self, key: S, value: V) -> Self {
self.0.insert(key.to_string(), value.into());
self
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[allow(clippy::duplicated_attributes)]
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Merge)]
#[serde(rename_all = "kebab-case")]
#[setters(
strip_option,
into,
generate_delegates(ty = "Step<Run>", field = "value"),
generate_delegates(ty = "Step<Use>", field = "value")
)]
pub struct StepValue {
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "if")]
#[merge(strategy = merge::option::overwrite_none)]
pub if_condition: Option<Expression>,
#[serde(skip_serializing_if = "Option::is_none")]
#[setters(skip)]
#[merge(strategy = merge::option::overwrite_none)]
pub uses: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub with: Option<Input>,
#[serde(skip_serializing_if = "Option::is_none")]
#[setters(skip)]
#[merge(strategy = merge::option::overwrite_none)]
pub run: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub shell: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub env: Option<Env>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub timeout_minutes: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub continue_on_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub working_directory: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub retry: Option<RetryStrategy>,
#[serde(skip_serializing_if = "Option::is_none")]
#[merge(strategy = merge::option::overwrite_none)]
pub artifacts: Option<Artifacts>,
}
impl StepValue {
pub fn run<T: ToString>(cmd: T) -> Self {
Self { run: Some(cmd.to_string()), ..Default::default() }
}
pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
owner: Owner,
repo: Repo,
version: Version,
) -> Self {
Self {
uses: Some(format!(
"{}/{}@{}",
owner.to_string(),
repo.to_string(),
version.to_string()
)),
..Default::default()
}
}
}
impl<T> Step<T> {
pub fn add_env<R: Into<Env>>(mut self, new_env: R) -> Self {
let mut env = self.value.env.take().unwrap_or_default();
env.0.extend(new_env.into().0);
self.value.env = Some(env);
self
}
}
impl Step<()> {
pub fn new(name: impl ToString) -> Self {
Self {
value: StepValue::default().name(name.to_string()),
marker: Default::default(),
}
}
pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
mut self,
owner: Owner,
repo: Repo,
version: Version,
) -> Step<Use> {
self.value.merge(StepValue::uses(owner, repo, version));
Step { value: self.value, marker: Default::default() }
}
pub fn run(mut self, cmd: impl ToString) -> Step<Run> {
self.value.merge(StepValue::run(cmd));
Step { value: self.value, marker: Default::default() }
}
}
impl Step<Use> {
pub fn checkout() -> Self {
Step::new("Checkout Code").uses("actions", "checkout", "v5")
}
pub fn add_with<I: Into<Input>>(mut self, new_with: I) -> Self {
let mut with = self.value.with.take().unwrap_or_default();
with.merge(new_with.into());
if with.0.is_empty() {
self.value.with = None;
} else {
self.value.with = Some(with);
}
self
}
}
impl<S1: ToString, S2: ToString> From<(S1, S2)> for Input {
fn from(value: (S1, S2)) -> Self {
let mut index_map: IndexMap<String, Value> = IndexMap::new();
index_map.insert(value.0.to_string(), Value::String(value.1.to_string()));
Self(index_map)
}
}
impl Step<Toolchain> {
pub fn toolchain() -> Self {
Self { value: Default::default(), marker: Toolchain::default() }
}
pub fn add_version(mut self, version: Version) -> Self {
self.marker.version.push(version);
self
}
pub fn add_component(mut self, component: Component) -> Self {
self.marker.components.push(component);
self
}
pub fn add_stable(mut self) -> Self {
self.marker.version.push(Version::Stable);
self
}
pub fn add_nightly(mut self) -> Self {
self.marker.version.push(Version::Nightly);
self
}
pub fn add_clippy(mut self) -> Self {
self.marker.components.push(Component::Clippy);
self
}
pub fn add_fmt(mut self) -> Self {
self.marker.components.push(Component::Rustfmt);
self
}
pub fn target(mut self, arch: Arch, vendor: Vendor, system: System, abi: Option<Abi>) -> Self {
self.marker.target = Some(Target { arch, vendor, system, abi });
self
}
}
impl StepType for Toolchain {
fn to_value(s: Step<Self>) -> StepValue {
let step: Step<Use> = s.marker.into();
StepValue::from(step)
}
}