diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..237913b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +env: + RUSTFLAGS: -Dwarnings +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main +permissions: + contents: read +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable, nightly + components: clippy, rustfmt + - run: cargo test --all-features --workspace + - run: cargo +nightly fmt --check + - run: cargo +nightly clippy --all-features --workspace diff --git a/.vscode/settings.json b/.vscode/settings.json index f9b7f55..c12f0d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,8 @@ { - "cSpell.words": ["Gollum", "quicktype", "rustfmt"] + "cSpell.words": [ + "Awarnings", + "Gollum", + "quicktype", + "rustfmt" + ] } diff --git a/Cargo.lock b/Cargo.lock index 4b26cab..c0e7fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "async-trait" @@ -131,6 +131,15 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "gh-workflow-gen" +version = "0.1.0" +dependencies = [ + "gh-workflow", + "indexmap", + "serde_json", +] + [[package]] name = "hashbrown" version = "0.15.0" @@ -252,9 +261,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "indexmap", "itoa", diff --git a/workspace/gh-workflow-gen/Cargo.toml b/workspace/gh-workflow-gen/Cargo.toml new file mode 100644 index 0000000..1cbe924 --- /dev/null +++ b/workspace/gh-workflow-gen/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gh-workflow-gen" +version = "0.1.0" +edition = "2021" + +[dependencies] + +serde_json = "1.0.132" +indexmap = "2.6.0" +gh-workflow = { path = "../gh-workflow" } diff --git a/workspace/gh-workflow-gen/src/main.rs b/workspace/gh-workflow-gen/src/main.rs new file mode 100644 index 0000000..c8d0682 --- /dev/null +++ b/workspace/gh-workflow-gen/src/main.rs @@ -0,0 +1,42 @@ +use gh_workflow::{Component, Job, Permissions, RustFlags, Step, Toolchain, Workflow}; + +fn main() { + let rust_flags = RustFlags::deny("warnings"); + + let build = Job::new("Build and Test") + .add_step(Step::checkout()) + .add_step( + Step::setup_rust() + .add_toolchain(Toolchain::Stable) + .add_toolchain(Toolchain::Nightly) + .components(vec![Component::Clippy, Component::Rustfmt]), + ) + .add_step(Step::cargo("test", vec!["--all-features", "--workspace"])) + .add_step(Step::cargo_nightly("fmt", vec!["--check"])) + .add_step(Step::cargo_nightly( + "clippy", + vec!["--all-features", "--workspace"], + )); + + Workflow::new("CI") + .env(rust_flags) + .permissions(Permissions::read()) + .on(vec![ + // TODO: enums + ("push", vec![("branches", vec!["main"])]), + ( + "pull_request", + vec![ + ("types", vec!["opened", "synchronize", "reopened"]), + ("branches", vec!["main"]), + ], + ), + ]) + .add_job("build", build) + .unwrap() + .generate(format!( + "{}/../../.github/workflows/ci.yml", + env!("CARGO_MANIFEST_DIR") + )) + .unwrap(); +} diff --git a/workspace/gh-workflow/src/lib.rs b/workspace/gh-workflow/src/lib.rs index fb97d05..8b317f0 100644 --- a/workspace/gh-workflow/src/lib.rs +++ b/workspace/gh-workflow/src/lib.rs @@ -1,4 +1,8 @@ pub mod error; +mod rust_flag; mod toolchain; pub(crate) mod workflow; + +pub use rust_flag::*; +pub use toolchain::*; pub use workflow::*; diff --git a/workspace/gh-workflow/src/rust_flag.rs b/workspace/gh-workflow/src/rust_flag.rs new file mode 100644 index 0000000..4da7f9e --- /dev/null +++ b/workspace/gh-workflow/src/rust_flag.rs @@ -0,0 +1,93 @@ +//! A type-safe representation of the Rust toolchain. + +use std::fmt::{Display, Formatter}; + +use crate::{Job, SetEnv, Step, Workflow}; + +#[derive(Clone)] +pub enum RustFlags { + Lint(String, Lint), + Combine(Box, Box), +} +#[derive(Clone)] +pub enum Lint { + Allow, + Warn, + Deny, + Forbid, + Codegen, + Experiment, +} + +impl core::ops::Add for RustFlags { + type Output = RustFlags; + + fn add(self, rhs: Self) -> Self::Output { + RustFlags::Combine(Box::new(self), Box::new(rhs)) + } +} + +impl RustFlags { + pub fn allow(name: S) -> Self { + RustFlags::Lint(name.to_string(), Lint::Allow) + } + + pub fn warn(name: S) -> Self { + RustFlags::Lint(name.to_string(), Lint::Warn) + } + + pub fn deny(name: S) -> Self { + RustFlags::Lint(name.to_string(), Lint::Deny) + } + + pub fn forbid(name: S) -> Self { + RustFlags::Lint(name.to_string(), Lint::Forbid) + } + + pub fn codegen(name: S) -> Self { + RustFlags::Lint(name.to_string(), Lint::Codegen) + } +} + +impl Display for RustFlags { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RustFlags::Lint(name, lint) => match lint { + Lint::Allow => write!(f, "-A{}", name), + Lint::Warn => write!(f, "-W{}", name), + Lint::Deny => write!(f, "-D{}", name), + Lint::Forbid => write!(f, "-F{}", name), + Lint::Codegen => write!(f, "-C{}", name), + Lint::Experiment => write!(f, "-Z{}", name), + }, + RustFlags::Combine(lhs, rhs) => write!(f, "{} {}", lhs, rhs), + } + } +} + +impl SetEnv for RustFlags { + fn apply(self, mut value: Job) -> Job { + let mut env = value.env.unwrap_or_default(); + env.insert("RUSTFLAGS".to_string(), self.to_string()); + value.env = Some(env); + value + } +} + +impl SetEnv for RustFlags { + fn apply(self, mut value: Workflow) -> Workflow { + let mut env = value.env.unwrap_or_default(); + env.insert("RUSTFLAGS".to_string(), self.to_string()); + value.env = Some(env); + value + } +} + +impl SetEnv> for RustFlags { + fn apply(self, mut value: Step) -> Step { + let mut env = value.env.unwrap_or_default(); + env.insert("RUSTFLAGS".to_string(), self.to_string()); + value.env = Some(env); + value + } +} diff --git a/workspace/gh-workflow/src/toolchain.rs b/workspace/gh-workflow/src/toolchain.rs index b4eb4bc..11695ba 100644 --- a/workspace/gh-workflow/src/toolchain.rs +++ b/workspace/gh-workflow/src/toolchain.rs @@ -1,38 +1,248 @@ +//! The typed version of https://github.com/actions-rust-lang/setup-rust-toolchain + +use std::fmt::{Display, Formatter}; + use derive_setters::Setters; -use crate::workflow::*; +use crate::{AddStep, Job, RustFlags, Step}; -/// -/// A type-safe representation of the Rust toolchain. -/// Instead of writing the github action for Rust by hand, we can use this -/// struct to generate the github action. -#[derive(Default)] -pub enum Version { - #[default] +#[derive(Clone)] +pub enum Toolchain { Stable, - Beta, Nightly, + Custom((u64, u64, u64)), } -impl Version { - pub fn to_string(&self) -> String { + +impl Display for Toolchain { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { - Version::Stable => "stable".to_string(), - Version::Beta => "beta".to_string(), - Version::Nightly => "nightly".to_string(), + Toolchain::Stable => write!(f, "stable"), + Toolchain::Nightly => write!(f, "nightly"), + Toolchain::Custom(s) => write!(f, "{}.{}.{}", s.0, s.1, s.2), } } } -#[derive(Setters, Default)] -pub struct RustToolchain { - version: Version, - fmt: bool, - clippy: bool, - // TODO: add more rust tool chain components +impl Toolchain { + pub fn new(major: u64, minor: u64, patch: u64) -> Self { + Toolchain::Custom((major, minor, patch)) + } +} + +#[derive(Clone, Debug)] +pub enum Component { + Clippy, + Rustfmt, + RustDoc, +} + +impl Display for Component { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let val = match self { + Component::Clippy => "clippy", + Component::Rustfmt => "rustfmt", + Component::RustDoc => "rust-doc", + }; + write!(f, "{}", val) + } +} + +#[derive(Clone)] +pub enum Arch { + X86_64, + Aarch64, + Arm, +} + +impl Display for Arch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let val = match self { + Arch::X86_64 => "x86_64", + Arch::Aarch64 => "aarch64", + Arch::Arm => "arm", + }; + write!(f, "{}", val) + } +} + +#[derive(Clone)] +pub enum Vendor { + Unknown, + Apple, + PC, +} + +impl Display for Vendor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let val = match self { + Vendor::Unknown => "unknown", + Vendor::Apple => "apple", + Vendor::PC => "pc", + }; + write!(f, "{}", val) + } +} + +#[derive(Clone)] +pub enum System { + Unknown, + Windows, + Linux, + Darwin, +} + +impl Display for System { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let val = match self { + System::Unknown => "unknown", + System::Windows => "windows", + System::Linux => "linux", + System::Darwin => "darwin", + }; + write!(f, "{}", val) + } +} + +#[derive(Clone)] +pub enum Abi { + Unknown, + Gnu, + Msvc, + Musl, +} + +impl Display for Abi { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let val = match self { + Abi::Unknown => "unknown", + Abi::Gnu => "gnu", + Abi::Msvc => "msvc", + Abi::Musl => "musl", + }; + write!(f, "{}", val) + } +} + +#[derive(Clone, Setters)] +pub struct Target { + arch: Arch, + vendor: Vendor, + system: System, + abi: Option, +} + +/// A Rust representation for the inputs of the setup-rust action. +/// More information can be found [here](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/action.yml). +/// NOTE: The public API should be close to the original action as much as +/// possible. +#[derive(Default, Clone, Setters)] +#[setters(strip_option)] +pub struct ToolchainStep { + pub toolchain: Vec, + pub target: Option, + pub components: Vec, + pub cache: Option, + pub cache_directories: Vec, + pub cache_workspaces: Vec, + pub cache_on_failure: Option, + pub cache_key: Option, + pub matcher: Option, + pub rust_flags: Option, + pub override_default: Option, +} + +impl ToolchainStep { + pub fn add_toolchain(mut self, version: Toolchain) -> Self { + self.toolchain.push(version); + self + } } -impl RustToolchain { - pub fn to_job(&self) -> Job { - todo!(); +impl AddStep for ToolchainStep { + fn apply(self, job: Job) -> Job { + let mut step = Step::uses("actions-rust-lang", "setup-rust-toolchain", 1); + + let toolchain = self + .toolchain + .iter() + .map(|t| match t { + Toolchain::Stable => "stable".to_string(), + Toolchain::Nightly => "nightly".to_string(), + Toolchain::Custom((major, minor, patch)) => { + format!("{}.{}.{}", major, minor, patch) + } + }) + .reduce(|acc, a| format!("{}, {}", acc, a)); + + if let Some(toolchain) = toolchain { + step = step.with(("toolchain", toolchain)); + } + + if let Some(target) = self.target { + let target = format!( + "{}-{}-{}{}", + target.arch, + target.vendor, + target.system, + target.abi.map(|v| v.to_string()).unwrap_or_default(), + ); + + step = step.with(("target", target)); + } + + if !self.components.is_empty() { + let components = self + .components + .iter() + .map(|c| c.to_string()) + .reduce(|acc, a| format!("{}, {}", acc, a)) + .unwrap_or_default(); + + step = step.with(("components", components)); + } + + if let Some(cache) = self.cache { + step = step.with(("cache", cache)); + } + + if !self.cache_directories.is_empty() { + let cache_directories = self + .cache_directories + .iter() + .fold("".to_string(), |acc, a| format!("{}\n{}", acc, a)); + + step = step.with(("cache-directories", cache_directories)); + } + + if !self.cache_workspaces.is_empty() { + let cache_workspaces = self + .cache_workspaces + .iter() + .fold("".to_string(), |acc, a| format!("{}\n{}", acc, a)); + + step = step.with(("cache-workspaces", cache_workspaces)); + } + + if let Some(cache_on_failure) = self.cache_on_failure { + step = step.with(("cache-on-failure", cache_on_failure)); + } + + if let Some(cache_key) = self.cache_key { + step = step.with(("cache-key", cache_key)); + } + + if let Some(matcher) = self.matcher { + step = step.with(("matcher", matcher)); + } + + if let Some(rust_flags) = self.rust_flags { + step = step.with(("rust-flags", rust_flags.to_string())); + } + + if let Some(override_default) = self.override_default { + step = step.with(("override", override_default)); + } + + job.add_step(step) } } diff --git a/workspace/gh-workflow/src/workflow.rs b/workspace/gh-workflow/src/workflow.rs index f3ad607..09f40c5 100644 --- a/workspace/gh-workflow/src/workflow.rs +++ b/workspace/gh-workflow/src/workflow.rs @@ -1,9 +1,15 @@ +#![allow(clippy::needless_update)] + +use std::fmt::Display; +use std::path::Path; + use derive_setters::Setters; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::error::{Error, Result}; +use crate::ToolchainStep; #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] @@ -45,16 +51,20 @@ pub enum Event { RepositoryDispatch, } -#[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Setters, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[setters(strip_option)] pub struct Workflow { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] + pub env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub run_name: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub on: Option, + #[setters(skip)] + pub on: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub permissions: Option, #[serde(skip_serializing_if = "IndexMap::is_empty")] @@ -66,22 +76,9 @@ pub struct Workflow { #[serde(skip_serializing_if = "Option::is_none")] pub secrets: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub env: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub timeout_minutes: Option, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "kebab-case", untagged)] -pub enum WorkflowOn { - // TODO: use type-safe enum instead of string - Single(String), - // TODO: use type-safe enum instead of string - Multiple(Vec), - // TODO: use type-safe enum instead of string - Map(IndexMap>>), -} - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct EventAction { @@ -93,36 +90,99 @@ pub struct EventAction { } impl Workflow { - pub fn new(name: String) -> Self { - Self { - name: Some(name), - permissions: Default::default(), - on: Default::default(), - run_name: Default::default(), - jobs: Default::default(), - concurrency: Default::default(), - defaults: Default::default(), - secrets: Default::default(), - env: Default::default(), - timeout_minutes: Default::default(), - } + pub fn new(name: T) -> Self { + Self { name: Some(name.to_string()), ..Default::default() } } pub fn to_string(&self) -> Result { Ok(serde_yaml::to_string(self)?) } - pub fn add_job(mut self, id: String, job: crate::Job) -> Result { - if self.jobs.contains_key(&id) { - return Err(Error::JobIdAlreadyExists(id)); + pub fn add_job>(mut self, id: T, job: J) -> Result { + let key = id.to_string(); + if self.jobs.contains_key(&key) { + return Err(Error::JobIdAlreadyExists(key.as_str().to_string())); } - self.jobs.insert(id, job); + self.jobs.insert(key, job.into()); Ok(self) } pub fn parse(yml: &str) -> Result { Ok(serde_yaml::from_str(yml)?) } + + pub fn generate>(self, path: T) -> Result<()> { + let path = path.as_ref(); + path.parent() + .map_or(Ok(()), std::fs::create_dir_all) + .map_err(Error::Io)?; + + std::fs::write(path, self.to_string()?).map_err(Error::Io)?; + println!( + "Generated workflow file: {}", + path.canonicalize()?.display() + ); + Ok(()) + } + + pub fn on(self, a: T) -> Self { + a.apply(self) + } + + pub fn env>(self, env: T) -> Self { + env.apply(self) + } +} + +// TODO: inline this conversion in actual usage +impl From<&str> for OneOrManyOrObject { + fn from(value: &str) -> Self { + OneOrManyOrObject::Single(value.to_string()) + } +} + +// TODO: inline this conversion in actual usage +impl From> for OneOrManyOrObject { + fn from(value: Vec<&str>) -> Self { + OneOrManyOrObject::Multiple(value.iter().map(|s| s.to_string()).collect()) + } +} + +// TODO: inline this conversion in actual usage +impl>> From> for OneOrManyOrObject { + fn from(value: Vec<(&str, V)>) -> Self { + OneOrManyOrObject::KeyValue( + value + .into_iter() + .map(|(s, v)| (s.to_string(), v.into())) + .collect(), + ) + } +} + +impl>> SetEvent for Vec<(S, W)> { + fn apply(self, mut workflow: Workflow) -> Workflow { + let val = self + .into_iter() + .map(|(s, w)| (s.to_string(), w.into())) + .collect(); + workflow.on = Some(OneOrManyOrObject::KeyValue(val)); + workflow + } +} + +impl SetEvent for Vec<&str> { + fn apply(self, workflow: Workflow) -> Workflow { + let on = self.into_iter().map(|s| s.to_string()).collect(); + Workflow { on: Some(OneOrManyOrObject::Multiple(on)), ..workflow } + } +} + +impl SetEvent for &str { + fn apply(self, workflow: Workflow) -> Workflow { + let on = self.to_string(); + Workflow { on: Some(OneOrManyOrObject::Single(on)), ..workflow } + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -165,13 +225,12 @@ pub struct Job { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] pub runs_on: Option>, - #[serde(skip_serializing_if = "Option::is_none", rename = "env")] - pub environment: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub strategy: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub steps: Option>, + pub steps: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub uses: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -191,6 +250,7 @@ pub struct Job { #[serde(skip_serializing_if = "Option::is_none")] pub defaults: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] pub env: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub continue_on_error: Option, @@ -200,6 +260,35 @@ pub struct Job { pub artifacts: Option, } +impl Job { + pub fn new(name: T) -> Self { + Self { + name: Some(name.to_string()), + runs_on: Some(OneOrManyOrObject::Single("ubuntu-latest".to_string())), + ..Default::default() + } + } + + pub fn add_step(self, step: S) -> Self { + step.apply(self) + } + + pub fn runs_on>(self, a: T) -> Self { + a.apply(self) + } + + pub fn env>(self, env: T) -> Self { + env.apply(self) + } +} + +impl SetRunner for T { + fn apply(self, mut job: Job) -> Job { + job.runs_on = Some(OneOrManyOrObject::Single(self.to_string())); + job + } +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(untagged)] @@ -217,23 +306,53 @@ pub enum OneOrMany { Multiple(Vec), } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum AnyStep { + Run(Step), + Use(Step), +} + +impl From> for AnyStep { + fn from(step: Step) -> Self { + AnyStep::Run(step) + } +} + +impl From> for AnyStep { + fn from(step: Step) -> Self { + AnyStep::Use(step) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Use; + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Run; + #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] #[setters(strip_option)] -pub struct Step { +pub struct Step { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] pub name: Option, #[serde(skip_serializing_if = "Option::is_none", rename = "if")] pub if_condition: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] pub uses: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub with: Option>, + #[setters(skip)] + with: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] pub run: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[setters(skip)] pub env: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub timeout_minutes: Option, @@ -245,11 +364,181 @@ pub struct Step { pub retry: Option, #[serde(skip_serializing_if = "Option::is_none")] pub artifacts: Option, + + #[serde(skip)] + marker: std::marker::PhantomData, +} + +impl Step { + pub fn name(mut self, name: S) -> Self { + self.name = Some(name.to_string()); + self + } + + pub fn env>(self, env: R) -> Self { + env.apply(self) + } +} + +impl AddStep for Step +where + Step: Into, +{ + fn apply(self, mut job: Job) -> Job { + let mut steps = job.steps.unwrap_or_default(); + steps.push(self.into()); + job.steps = Some(steps); + + job + } +} + +impl Step { + pub fn run(cmd: T) -> Self { + Step { run: Some(cmd.to_string()), ..Default::default() } + } + + pub fn cargo(cmd: T, params: Vec

) -> Self { + Step::run(format!( + "cargo {} {}", + cmd.to_string(), + params + .iter() + .map(|a| a.to_string()) + .reduce(|a, b| { format!("{} {}", a, b) }) + .unwrap_or_default() + )) + } + + pub fn cargo_nightly(cmd: T, params: Vec

) -> Self { + Step::cargo(format!("+nightly {}", cmd.to_string()), params) + } +} + +impl Step { + pub fn uses(owner: Owner, repo: Repo, version: u64) -> Self { + Step { + uses: Some(format!( + "{}/{}@v{}", + owner.to_string(), + repo.to_string(), + version + )), + ..Default::default() + } + } + + pub fn with(self, item: K) -> Self { + item.apply(self) + } + + pub fn checkout() -> Self { + Step::uses("actions", "checkout", 4).name("Checkout Code") + } + + pub fn setup_rust() -> ToolchainStep { + ToolchainStep::default() + } +} + +impl SetInput for IndexMap { + fn apply(self, mut step: Step) -> Step { + let mut with = step.with.unwrap_or_default(); + with.extend(self); + step.with = Some(with); + step + } +} + +impl SetInput for (S1, S2) { + fn apply(self, mut step: Step) -> Step { + let mut with = step.with.unwrap_or_default(); + with.insert(self.0.to_string(), Value::String(self.1.to_string())); + step.with = Some(with); + step + } +} + +impl SetEnv for (S1, S2) { + fn apply(self, mut value: Job) -> Job { + let mut index_map: IndexMap = value.env.unwrap_or_default(); + index_map.insert(self.0.to_string(), self.1.to_string()); + value.env = Some(index_map); + value + } +} + +impl From> for Step { + fn from(value: Step) -> Self { + Step { + id: value.id, + name: value.name, + if_condition: value.if_condition, + uses: value.uses, + with: value.with, + run: value.run, + env: value.env, + timeout_minutes: value.timeout_minutes, + continue_on_error: value.continue_on_error, + working_directory: value.working_directory, + retry: value.retry, + artifacts: value.artifacts, + marker: Default::default(), + } + } } -impl Step { - pub fn new(step: String) -> Self { - Self { run: Some(step), name: None, ..Default::default() } +impl From> for Step { + fn from(value: Step) -> Self { + Step { + id: value.id, + name: value.name, + if_condition: value.if_condition, + uses: value.uses, + with: value.with, + run: value.run, + env: value.env, + timeout_minutes: value.timeout_minutes, + continue_on_error: value.continue_on_error, + working_directory: value.working_directory, + retry: value.retry, + artifacts: value.artifacts, + marker: Default::default(), + } + } +} + +/// Set the `env` for Step, Job or Workflows +pub trait SetEnv { + fn apply(self, value: Value) -> Value; +} + +/// Set the `run` for a Job +pub trait SetRunner { + fn apply(self, job: Job) -> Job; +} + +/// Sets the event for a Workflow +pub trait SetEvent { + fn apply(self, workflow: Workflow) -> Workflow; +} + +/// Sets the input for a Step that uses another action +pub trait SetInput { + fn apply(self, step: Step) -> Step; +} + +/// Inserts a step into a job +pub trait AddStep { + fn apply(self, job: Job) -> Job; +} + +impl SetEnv> for (S1, S2) { + fn apply(self, mut step: Step) -> Step { + let mut index_map: IndexMap = step.with.unwrap_or_default(); + index_map.insert(self.0.to_string(), Value::String(self.1.to_string())); + step.with = Some(index_map); + step } } @@ -358,6 +647,16 @@ pub struct Permissions { pub event_specific: Option>, } +impl Permissions { + pub fn read() -> Self { + Self { contents: Some(PermissionLevel::Read), ..Default::default() } + } + + pub fn write() -> Self { + Self { contents: Some(PermissionLevel::Write), ..Default::default() } + } +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] pub enum PermissionLevel { @@ -421,6 +720,12 @@ pub struct RetryDefaults { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub struct Expression(String); +impl Expression { + pub fn new(expr: T) -> Self { + Self(expr.to_string()) + } +} + #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] #[setters(strip_option)]