diff --git a/Cargo.lock b/Cargo.lock index 5095d5c..10269cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "strum_macros", ] [[package]] @@ -151,6 +152,12 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "ident_case" version = "1.0.1" @@ -293,6 +300,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" version = "1.0.18" @@ -356,6 +369,19 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.87", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/README.md b/README.md index 4459f7e..6dad455 100644 --- a/README.md +++ b/README.md @@ -27,48 +27,13 @@ To use **Rust GitHub Workflows** in your project, add it to your `Cargo.toml`: gh-workflow = "0.1" ``` -Then you can start creating GitHub Actions in your [build.rs](https://github.com/tailcallhq/rust-gh-workflows/blob/main/workspace/gh-workflow-gen/build.rs). - -## 👷 Usage - -- Simply add a `build.rs` file to your project's root directory. -- Add the following code to generate the GitHub Actions workflow: - -```rust - use rust_gh_workflows::*; - - fn main() { - // Create a workflow - let workflow = Workflow::new("CI") - .permissions(Permissions::read()) - .on(Event::default().push(Push::default().branch("main")) - .add_job( - "build", - Job::new("Build and Test") - .add_step(Step::checkout()) - .add_step(Step::setup_rust().add_toolchain(Toolchain::Stable)) - .add_step(Step::cargo("test", vec!["--all-features", "--workspace"])) - ) - .unwrap(); - - // Generate the ci.yml - workflow.generate().unwrap(); - } - ``` +or via CLI - To view a fully functional example, check out the [build.rs](https://github.com/tailcallhq/rust-gh-workflows/blob/main/workspace/gh-workflow-gen/build.rs) of this project. - -- Run `cargo build` to generate the GitHub Actions workflow. - -**Workspace** - -- The `workspace` directory contains the `gh-workflow-gen` crate, which generates the workflow. - -## 🛠️ Roadmap +```bash +cargo add --build gh-workflow +``` -- [ ] Support for Automated Cargo Releases -- [ ] Improve Type Safety of Nightly Builds -- [ ] Add Rust Docs for the API +Then you can start creating GitHub Actions in your [build.rs](https://github.com/tailcallhq/rust-gh-workflows/blob/main/workspace/gh-workflow-gen/build.rs). ## 💡 Why Rust? diff --git a/workspace/gh-workflow-gen/build.rs b/workspace/gh-workflow-gen/build.rs index 6e9e7b1..dbbf323 100644 --- a/workspace/gh-workflow-gen/build.rs +++ b/workspace/gh-workflow-gen/build.rs @@ -1,9 +1,51 @@ -use gh_workflow::generate::Generate; use gh_workflow::*; +use toolchain::Toolchain; fn main() { - Generate::new(Workflow::setup_rust()) - .name("ci.yml") + let job = Job::new("Build and Test") + .add_step(Step::checkout()) + .add_step( + Toolchain::default() + .add_stable() + .add_nightly() + .add_clippy() + .add_fmt(), + ) + .add_step( + Cargo::new("test") + .args("--all-features --workspace") + .name("Cargo Test"), + ) + .add_step( + Cargo::new("fmt") + .nightly() + .args("--check") + .name("Cargo Fmt"), + ) + .add_step( + Cargo::new("clippy") + .nightly() + .args("--all-features --workspace -- -D warnings") + .name("Cargo Clippy"), + ); + + let event = Event::default() + .push(Push::default().add_branch("main")) + .pull_request_target( + PullRequestTarget::default() + .open() + .synchronize() + .reopen() + .add_branch("main"), + ); + + let flags = RustFlags::deny("warnings"); + + Workflow::new("Build and Test") + .env(flags) + .permissions(Permissions::read()) + .on(event) + .add_job("build", job) .generate() .unwrap(); } diff --git a/workspace/gh-workflow/Cargo.toml b/workspace/gh-workflow/Cargo.toml index a7ad603..ac25702 100644 --- a/workspace/gh-workflow/Cargo.toml +++ b/workspace/gh-workflow/Cargo.toml @@ -5,19 +5,20 @@ edition = "2021" description = "A type-safe GitHub Actions workflow generator" license = "Apache-2.0" -documentation = "https://github.com/tailcallhq/rust-gh-workflows" +documentation = "https://docs.rs/gh-workflow" homepage = "https://github.com/tailcallhq/rust-gh-workflows" repository = "https://github.com/tailcallhq/rust-gh-workflows" [dependencies] async-trait = "0.1.83" -derive_more = { version = "1.0.0", features = ["from"] } +derive_more = { version = "1.0.0", features = ["from", "deref", "deref_mut"] } derive_setters = "0.1.6" indexmap = { version = "2.6.0", features = ["serde"] } merge = "0.1.0" serde = { version = "1.0.210", features = ["derive"] } serde_json = { version = "1.0.128" } serde_yaml = "0.9.34" +strum_macros = "0.26.4" [dev-dependencies] insta = "1.40.0" diff --git a/workspace/gh-workflow/src/cargo.rs b/workspace/gh-workflow/src/cargo.rs new file mode 100644 index 0000000..49e8296 --- /dev/null +++ b/workspace/gh-workflow/src/cargo.rs @@ -0,0 +1,70 @@ +use derive_setters::Setters; + +use crate::toolchain::Version; +use crate::StepValue; + +#[derive(Setters)] +#[setters(strip_option, into)] +pub struct Cargo { + /// The command to be executed for eg: fmt, clippy, build, test, etc. + pub command: String, + + /// The unique identifier of the Step. + pub id: Option, + + /// Name of the Step + pub name: Option, + + /// Toolchain to be used for example `+nightly`. + pub toolchain: Option, + + /// Arguments to be passed to the cargo command. + pub args: Option, +} + +impl Cargo { + pub fn new(cmd: T) -> Cargo { + Cargo { + command: cmd.to_string(), + id: Default::default(), + name: Default::default(), + toolchain: Default::default(), + args: Default::default(), + } + } + + pub fn nightly(mut self) -> Self { + self.toolchain = Some(Version::Nightly); + self + } +} + +impl From for StepValue { + fn from(value: Cargo) -> Self { + let mut command = vec!["cargo".to_string()]; + + if let Some(toolchain) = value.toolchain { + command.push(format!("+{}", toolchain)); + } + + command.push(value.command); + + if let Some(args) = value.args { + if !args.is_empty() { + command.push(args); + } + } + + let mut step = StepValue::run(command.join(" ")); + + if let Some(id) = value.id { + step = step.id(id); + } + + if let Some(name) = value.name { + step = step.name(name); + } + + step + } +} diff --git a/workspace/gh-workflow/src/event.rs b/workspace/gh-workflow/src/event.rs index 3c02ffe..c0ce99c 100644 --- a/workspace/gh-workflow/src/event.rs +++ b/workspace/gh-workflow/src/event.rs @@ -2,12 +2,10 @@ use derive_setters::Setters; use merge::Merge; use serde::{Deserialize, Serialize}; -use crate::SetEvent; - #[derive(Default, Setters, Debug, Serialize, Deserialize, Clone, Merge, PartialEq, Eq)] #[serde(rename_all = "snake_case")] -#[setters(strip_option)] -pub struct EventValue { +#[setters(strip_option, into)] +pub struct Event { #[serde(skip_serializing_if = "Option::is_none")] pub push: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -17,53 +15,22 @@ pub struct EventValue { // TODO: add all more events } -pub struct Event(A); - -impl Event { - pub fn push() -> Self { - Event(Push::default()) - } -} - -impl Event { - pub fn pull_request() -> Self { - Event(PullRequest::default()) - } -} - -impl Event { - pub fn pull_request_target() -> Self { - Event(PullRequestTarget::default()) - } -} - -impl> SetEvent for Event { - fn apply(self, mut workflow: crate::Workflow) -> crate::Workflow { - let mut on: EventValue = self.0.into(); - if let Some(other) = workflow.on { - on.merge(other); - } - workflow.on = Some(on); - workflow - } -} - #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct Push { branches: Vec, } -impl Event { - pub fn branch(mut self, branch: S) -> Self { - self.0.branches.push(branch.to_string()); +impl Push { + pub fn add_branch(mut self, branch: S) -> Self { + self.branches.push(branch.to_string()); self } } -impl From for EventValue { +impl From for Event { fn from(value: Push) -> Self { - EventValue::default().push(value) + Event::default().push(value) } } @@ -76,18 +43,18 @@ pub struct PullRequest { branches: Option>, } -impl Event { - pub fn branch(mut self, branch: S) -> Self { - let mut branches = self.0.branches.unwrap_or_default(); +impl PullRequest { + pub fn add_branch(mut self, branch: S) -> Self { + let mut branches = self.branches.unwrap_or_default(); branches.push(branch.to_string()); - self.0.branches = Some(branches); + self.branches = Some(branches); self } fn add_type(mut self, ty: &str) -> Self { - let mut types = self.0.types.unwrap_or_default(); + let mut types = self.types.unwrap_or_default(); types.push(ty.to_string()); - self.0.types = Some(types); + self.types = Some(types); self } @@ -104,9 +71,9 @@ impl Event { } } -impl From for EventValue { +impl From for Event { fn from(value: PullRequest) -> Self { - EventValue::default().pull_request(value) + Event::default().pull_request(value) } } #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -118,18 +85,18 @@ pub struct PullRequestTarget { branches: Option>, } -impl Event { - pub fn branch(mut self, branch: S) -> Self { - let mut branches = self.0.branches.unwrap_or_default(); +impl PullRequestTarget { + pub fn add_branch(mut self, branch: S) -> Self { + let mut branches = self.branches.unwrap_or_default(); branches.push(branch.to_string()); - self.0.branches = Some(branches); + self.branches = Some(branches); self } fn add_type(mut self, ty: &str) -> Self { - let mut types = self.0.types.unwrap_or_default(); + let mut types = self.types.unwrap_or_default(); types.push(ty.to_string()); - self.0.types = Some(types); + self.types = Some(types); self } @@ -146,8 +113,8 @@ impl Event { } } -impl From for EventValue { +impl From for Event { fn from(value: PullRequestTarget) -> Self { - EventValue::default().pull_request_target(value) + Event::default().pull_request_target(value) } } diff --git a/workspace/gh-workflow/src/lib.rs b/workspace/gh-workflow/src/lib.rs index 79e567d..e373feb 100644 --- a/workspace/gh-workflow/src/lib.rs +++ b/workspace/gh-workflow/src/lib.rs @@ -1,11 +1,12 @@ +mod cargo; pub mod error; mod event; pub mod generate; mod rust_flag; -mod toolchain; +pub mod toolchain; pub(crate) mod workflow; +pub use cargo::*; pub use event::*; 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 index 4da7f9e..3b348fe 100644 --- a/workspace/gh-workflow/src/rust_flag.rs +++ b/workspace/gh-workflow/src/rust_flag.rs @@ -2,7 +2,10 @@ use std::fmt::{Display, Formatter}; -use crate::{Job, SetEnv, Step, Workflow}; +use indexmap::IndexMap; +use serde_json::Value; + +use crate::Env; #[derive(Clone)] pub enum RustFlags { @@ -65,29 +68,10 @@ impl Display for RustFlags { } } -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 +impl From for Env { + fn from(value: RustFlags) -> Self { + let mut env = IndexMap::default(); + env.insert("RUSTFLAGS".to_string(), Value::from(value.to_string())); + Env::from(env) } } diff --git a/workspace/gh-workflow/src/toolchain.rs b/workspace/gh-workflow/src/toolchain.rs index 97e63a6..aa6fcb7 100644 --- a/workspace/gh-workflow/src/toolchain.rs +++ b/workspace/gh-workflow/src/toolchain.rs @@ -4,28 +4,28 @@ use std::fmt::{Display, Formatter}; use derive_setters::Setters; -use crate::{AddStep, Job, RustFlags, Step}; +use crate::{Input, RustFlags, StepValue}; #[derive(Clone)] -pub enum Toolchain { +pub enum Version { Stable, Nightly, Custom((u64, u64, u64)), } -impl Display for Toolchain { +impl Display for Version { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { - Toolchain::Stable => write!(f, "stable"), - Toolchain::Nightly => write!(f, "nightly"), - Toolchain::Custom(s) => write!(f, "{}.{}.{}", s.0, s.1, s.2), + Version::Stable => write!(f, "stable"), + Version::Nightly => write!(f, "nightly"), + Version::Custom(s) => write!(f, "{}.{}.{}", s.0, s.1, s.2), } } } -impl Toolchain { +impl Version { pub fn new(major: u64, minor: u64, patch: u64) -> Self { - Toolchain::Custom((major, minor, patch)) + Version::Custom((major, minor, patch)) } } @@ -138,9 +138,9 @@ pub struct Target { /// 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, +#[setters(strip_option, into)] +pub struct Toolchain { + pub toolchain: Vec, #[setters(skip)] pub target: Option, pub components: Vec, @@ -154,8 +154,8 @@ pub struct ToolchainStep { pub override_default: Option, } -impl ToolchainStep { - pub fn add_toolchain(mut self, version: Toolchain) -> Self { +impl Toolchain { + pub fn add_version(mut self, version: Version) -> Self { self.toolchain.push(version); self } @@ -165,22 +165,22 @@ impl ToolchainStep { self } - pub fn with_stable_toolchain(mut self) -> Self { - self.toolchain.push(Toolchain::Stable); + pub fn add_stable(mut self) -> Self { + self.toolchain.push(Version::Stable); self } - pub fn with_nightly_toolchain(mut self) -> Self { - self.toolchain.push(Toolchain::Nightly); + pub fn add_nightly(mut self) -> Self { + self.toolchain.push(Version::Nightly); self } - pub fn with_clippy(mut self) -> Self { + pub fn add_clippy(mut self) -> Self { self.components.push(Component::Clippy); self } - pub fn with_fmt(mut self) -> Self { + pub fn add_fmt(mut self) -> Self { self.components.push(Component::Rustfmt); self } @@ -191,28 +191,30 @@ impl ToolchainStep { } } -impl AddStep for ToolchainStep { - fn apply(self, job: Job) -> Job { - let mut step = - Step::uses("actions-rust-lang", "setup-rust-toolchain", 1).name("Setup Rust Toolchain"); +impl From for StepValue { + fn from(value: Toolchain) -> Self { + let mut step = StepValue::uses("actions-rust-lang", "setup-rust-toolchain", 1) + .name("Setup Rust Toolchain"); - let toolchain = self + let toolchain = value .toolchain .iter() .map(|t| match t { - Toolchain::Stable => "stable".to_string(), - Toolchain::Nightly => "nightly".to_string(), - Toolchain::Custom((major, minor, patch)) => { + Version::Stable => "stable".to_string(), + Version::Nightly => "nightly".to_string(), + Version::Custom((major, minor, patch)) => { format!("{}.{}.{}", major, minor, patch) } }) .reduce(|acc, a| format!("{}, {}", acc, a)); + let mut input = Input::default(); + if let Some(toolchain) = toolchain { - step = step.with(("toolchain", toolchain)); + input = input.add("toolchain", toolchain); } - if let Some(target) = self.target { + if let Some(target) = value.target { let target = format!( "{}-{}-{}{}", target.arch, @@ -221,62 +223,64 @@ impl AddStep for ToolchainStep { target.abi.map(|v| v.to_string()).unwrap_or_default(), ); - step = step.with(("target", target)); + input = input.add("target", target); } - if !self.components.is_empty() { - let components = self + if !value.components.is_empty() { + let components = value .components .iter() .map(|c| c.to_string()) .reduce(|acc, a| format!("{}, {}", acc, a)) .unwrap_or_default(); - step = step.with(("components", components)); + input = input.add("components", components); } - if let Some(cache) = self.cache { - step = step.with(("cache", cache)); + if let Some(cache) = value.cache { + input = input.add("cache", cache); } - if !self.cache_directories.is_empty() { - let cache_directories = self + if !value.cache_directories.is_empty() { + let cache_directories = value .cache_directories .iter() .fold("".to_string(), |acc, a| format!("{}\n{}", acc, a)); - step = step.with(("cache-directories", cache_directories)); + input = input.add("cache-directories", cache_directories); } - if !self.cache_workspaces.is_empty() { - let cache_workspaces = self + if !value.cache_workspaces.is_empty() { + let cache_workspaces = value .cache_workspaces .iter() .fold("".to_string(), |acc, a| format!("{}\n{}", acc, a)); - step = step.with(("cache-workspaces", cache_workspaces)); + input = input.add("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_on_failure) = value.cache_on_failure { + input = input.add("cache-on-failure", cache_on_failure); } - if let Some(cache_key) = self.cache_key { - step = step.with(("cache-key", cache_key)); + if let Some(cache_key) = value.cache_key { + input = input.add("cache-key", cache_key); } - if let Some(matcher) = self.matcher { - step = step.with(("matcher", matcher)); + if let Some(matcher) = value.matcher { + input = input.add("matcher", matcher); } - if let Some(rust_flags) = self.rust_flags { - step = step.with(("rust-flags", rust_flags.to_string())); + if let Some(rust_flags) = value.rust_flags { + input = input.add("rust-flags", rust_flags.to_string()); } - if let Some(override_default) = self.override_default { - step = step.with(("override", override_default)); + if let Some(override_default) = value.override_default { + input = input.add("override", override_default); } - job.add_step(step) + step = step.with(input); + + step } } diff --git a/workspace/gh-workflow/src/workflow.rs b/workspace/gh-workflow/src/workflow.rs index fa30929..f460205 100644 --- a/workspace/gh-workflow/src/workflow.rs +++ b/workspace/gh-workflow/src/workflow.rs @@ -1,34 +1,43 @@ -#![allow(clippy::needless_update)] +//! +//! The serde representation of Github Actions Workflow. use std::fmt::Display; use derive_setters::Setters; use indexmap::IndexMap; +use merge::Merge; use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; +use serde_json::Value; use crate::error::Result; use crate::generate::Generate; -use crate::{Event, EventValue, RustFlags, ToolchainStep}; +use crate::Event; + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct Jobs(IndexMap); +impl Jobs { + pub fn insert(&mut self, key: String, value: Job) { + self.0.insert(key, value); + } +} #[derive(Debug, Default, Setters, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] 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>, + pub env: Option, #[serde(skip_serializing_if = "Option::is_none")] pub run_name: Option, #[serde(skip_serializing_if = "Option::is_none")] - #[setters(skip)] - pub on: Option, + pub on: Option, #[serde(skip_serializing_if = "Option::is_none")] pub permissions: Option, - #[serde(skip_serializing_if = "IndexMap::is_empty")] - pub jobs: IndexMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub jobs: Option, #[serde(skip_serializing_if = "Option::is_none")] pub concurrency: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -59,8 +68,11 @@ impl Workflow { pub fn add_job>(mut self, id: T, job: J) -> Self { let key = id.to_string(); + let mut jobs = self.jobs.unwrap_or_default(); - self.jobs.insert(key, job.into()); + jobs.insert(key, job.into()); + + self.jobs = Some(jobs); self } @@ -72,51 +84,20 @@ impl Workflow { Generate::new(self).generate() } - pub fn on(self, a: T) -> Self { - a.apply(self) + pub fn add_event>(mut self, that: T) -> Self { + let mut this = self.on.unwrap_or_default(); + let that: Event = that.into(); + this.merge(that); + self.on = Some(this); + self } - pub fn env>(self, env: T) -> Self { - env.apply(self) - } + pub fn add_env>(mut self, new_env: T) -> Self { + let mut env = self.env.unwrap_or_default(); - pub fn setup_rust() -> Self { - let build_job = Job::new("Build and Test") - .add_step(Step::checkout()) - .add_step( - Step::setup_rust() - .with_stable_toolchain() - .with_nightly_toolchain() - .with_clippy() - .with_fmt(), - ) - // TODO: make it type-safe - .add_step(Step::cargo("test", vec!["--all-features", "--workspace"]).name("Cargo Test")) - .add_step(Step::cargo_nightly("fmt", vec!["--check"]).name("Cargo Fmt")) - .add_step( - Step::cargo_nightly( - "clippy", - vec!["--all-features", "--workspace", "--", "-D warnings"], - ) - .name("Cargo Clippy"), - ); - - let push_event = Event::push().branch("main"); - - let pr_event = Event::pull_request_target() - .open() - .synchronize() - .reopen() - .branch("main"); - - let rust_flags = RustFlags::deny("warnings"); - - Workflow::new("Build and Test") - .env(rust_flags) - .permissions(Permissions::read()) - .on(push_event) - .on(pr_event) - .add_job("build", build_job) + env.0.extend(new_env.into().0); + self.env = Some(env); + self } } @@ -132,9 +113,18 @@ pub enum ActivityType { #[serde(transparent)] pub struct RunsOn(Value); +impl From for RunsOn +where + T: Into, +{ + fn from(value: T) -> Self { + RunsOn(value.into()) + } +} + #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Job { #[serde(skip_serializing_if = "Option::is_none")] pub needs: Option, @@ -143,12 +133,11 @@ 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")] 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")] @@ -168,8 +157,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>, + pub env: Option, #[serde(skip_serializing_if = "Option::is_none")] pub continue_on_error: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -187,94 +175,77 @@ impl Job { } } - 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) + pub fn add_step>(mut self, step: S) -> Self { + let mut steps = self.steps.unwrap_or_default(); + steps.push(step.into()); + self.steps = Some(steps); + self } } -pub trait AddRunsOn { - fn apply(self, job: Job) -> Job; +#[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct Step { + value: StepValue, + #[serde(skip)] + marker: std::marker::PhantomData, } -impl From<&str> for RunsOn { - fn from(value: &str) -> Self { - RunsOn(Value::String(value.to_string())) +impl From> for StepValue { + fn from(step: Step) -> Self { + step.value } } -impl From> for RunsOn { - fn from(value: Vec<&str>) -> Self { - RunsOn(Value::Array( - value - .into_iter() - .map(|v| v.to_string()) - .map(Value::String) - .collect(), - )) +impl From> for StepValue { + fn from(step: Step) -> Self { + step.value } } -impl> AddRunsOn for Vec<(&str, V)> { - fn apply(self, mut job: Job) -> Job { - let val = self.into_iter().map(|(a, b)| (a.to_string(), b.into())); - let mut map = Map::new(); - for (k, v) in val { - map.insert(k.to_string(), v.0); - } +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Use; - job.runs_on = Some(RunsOn(Value::Object(map))); - job - } -} +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Run; -impl> AddRunsOn for V { - fn apply(self, mut job: Job) -> Job { - job.runs_on = Some(self.into()); - job +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +#[serde(transparent)] +pub struct Env(IndexMap); +impl From> for Env { + fn from(value: IndexMap) -> Self { + Env(value) } } -#[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) +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(transparent)] +pub struct Input(IndexMap); +impl From> for Input { + fn from(value: IndexMap) -> Self { + Input(value) } } -impl From> for AnyStep { - fn from(step: Step) -> Self { - AnyStep::Use(step) +impl Input { + pub fn add>(mut self, key: S, value: V) -> Self { + self.0.insert(key.to_string(), value.into()); + self } } - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct Use; - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct Run; - +#[allow(clippy::duplicated_attributes)] #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] -pub struct Step { +#[setters( + strip_option, + into, + generate_delegates(ty = "Step", field = "value"), + generate_delegates(ty = "Step", field = "value") +)] +pub struct StepValue { #[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, @@ -282,83 +253,31 @@ pub struct Step { #[setters(skip)] pub uses: Option, #[serde(skip_serializing_if = "Option::is_none")] - #[setters(skip)] - with: Option>, + 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>, + pub env: Option, #[serde(skip_serializing_if = "Option::is_none")] pub timeout_minutes: Option, #[serde(skip_serializing_if = "Option::is_none")] pub continue_on_error: Option, #[serde(skip_serializing_if = "Option::is_none")] - #[setters(skip)] pub working_directory: Option, #[serde(skip_serializing_if = "Option::is_none")] 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) - } - pub fn working_directory(mut self, working_directory: S) -> Self { - self.working_directory = Some(working_directory.to_string()); - 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 { +impl StepValue { pub fn run(cmd: T) -> Self { - Step { run: Some(cmd.to_string()), ..Default::default() } + StepValue { 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 { + StepValue { uses: Some(format!( "{}/{}@v{}", owner.to_string(), @@ -368,118 +287,40 @@ impl Step { ..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 Step { + pub fn run(cmd: T) -> Self { + Step { value: StepValue::run(cmd), marker: Default::default() } } } -impl From> for Step { - fn from(value: Step) -> Self { +impl Step { + pub fn uses(owner: Owner, repo: Repo, version: u64) -> 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, + value: StepValue::uses(owner, repo, version), marker: 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(), - } + pub fn checkout() -> Step { + Step::uses("actions", "checkout", 4).name("Checkout Code") } } -/// 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 From<(S1, S2)> for Input { + fn from(value: (S1, S2)) -> Self { + let mut index_map: IndexMap = IndexMap::new(); + index_map.insert(value.0.to_string(), Value::String(value.1.to_string())); + Input(index_map) + } } -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 +impl From<(S1, S2)> for Env { + fn from(value: (S1, S2)) -> Self { + let mut index_map: IndexMap = IndexMap::new(); + index_map.insert(value.0.to_string(), Value::String(value.1.to_string())); + Env(index_map) } } @@ -495,13 +336,13 @@ pub enum Runner { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Container { pub image: String, #[serde(skip_serializing_if = "Option::is_none")] pub credentials: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub env: Option>, + pub env: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ports: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -514,7 +355,7 @@ pub struct Container { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Credentials { pub username: String, pub password: String, @@ -529,7 +370,7 @@ pub enum Port { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Volume { pub source: String, pub destination: String, @@ -551,7 +392,7 @@ impl Volume { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Concurrency { pub group: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -562,7 +403,7 @@ pub struct Concurrency { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Permissions { #[serde(skip_serializing_if = "Option::is_none")] pub actions: Option, @@ -607,7 +448,7 @@ pub enum PermissionLevel { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Strategy { #[serde(skip_serializing_if = "Option::is_none")] pub matrix: Option, @@ -619,7 +460,7 @@ pub struct Strategy { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Environment { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -628,7 +469,7 @@ pub struct Environment { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Defaults { #[serde(skip_serializing_if = "Option::is_none")] pub run: Option, @@ -640,7 +481,7 @@ pub struct Defaults { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct RunDefaults { #[serde(skip_serializing_if = "Option::is_none")] pub shell: Option, @@ -648,9 +489,8 @@ pub struct RunDefaults { pub working_directory: Option, } -#[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] pub struct RetryDefaults { #[serde(skip_serializing_if = "Option::is_none")] pub max_attempts: Option, @@ -667,16 +507,15 @@ impl Expression { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Secret { pub required: bool, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, } -#[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] pub struct RetryStrategy { #[serde(skip_serializing_if = "Option::is_none")] pub max_attempts: Option, @@ -684,7 +523,7 @@ pub struct RetryStrategy { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Artifacts { #[serde(skip_serializing_if = "Option::is_none")] pub upload: Option>, @@ -694,7 +533,7 @@ pub struct Artifacts { #[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] -#[setters(strip_option)] +#[setters(strip_option, into)] pub struct Artifact { pub name: String, pub path: String,