feat: add rust actions
All checks were successful
continuous-integration/drone/push Build is passing

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2024-04-09 22:57:01 +02:00
parent 9999fca9b0
commit 3878e6bc0a
10 changed files with 464 additions and 11 deletions

View File

@@ -0,0 +1,9 @@
use semver::Version;
use crate::ActionConfig;
pub trait Action {
fn enabled(&self, config: &ActionConfig) -> anyhow::Result<bool>;
fn name(&self) -> String;
fn execute(&self, version: &Version) -> anyhow::Result<()>;
}

View File

@@ -0,0 +1,49 @@
use std::ops::Deref;
use anyhow::Context;
pub enum ActionConfig {
Actual { doc: yaml_rust2::Yaml },
None,
}
impl Deref for ActionConfig {
type Target = yaml_rust2::Yaml;
fn deref(&self) -> &Self::Target {
match &self {
ActionConfig::Actual { doc } => doc,
ActionConfig::None => &yaml_rust2::Yaml::BadValue,
}
}
}
impl TryFrom<&str> for ActionConfig {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut cuddle = yaml_rust2::YamlLoader::load_from_str(value)?;
if cuddle.len() != 1 {
anyhow::bail!("cuddle.yaml can only be 1 document wide");
}
let doc = cuddle.pop().unwrap();
let doc = doc["please"]["actions"].clone();
if doc.is_badvalue() {
return Ok(Self::None);
}
Ok(Self::Actual { doc })
}
}
impl ActionConfig {
pub fn parse() -> anyhow::Result<Self> {
let cuddle_yaml =
std::fs::read_to_string("cuddle.yaml").context("failed to read cuddle.yaml")?;
Self::try_from(cuddle_yaml.as_str())
}
}

View File

@@ -0,0 +1,45 @@
pub(crate) mod actions;
mod config;
mod rust_action;
use std::{ops::Deref, sync::Arc};
use catalog::RustAction;
pub use config::ActionConfig;
use semver::Version;
pub mod catalog {
pub use crate::rust_action::*;
}
pub struct Action(Arc<dyn actions::Action>);
impl Deref for Action {
type Target = Arc<dyn actions::Action>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct Actions(Vec<Action>);
impl Actions {
pub fn from_cuddle() -> anyhow::Result<Self> {
let config = ActionConfig::parse()?;
Ok(Self(
vec![Action(Arc::new(RustAction::new()))]
.into_iter()
.filter(|a| a.enabled(&config).unwrap_or_default())
.collect(),
))
}
pub fn execute(&self, version: &Version) -> anyhow::Result<()> {
for action in &self.0 {
action.execute(version)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,237 @@
use std::io::Write;
use anyhow::Context;
use crate::{actions::Action, ActionConfig};
#[derive(Default, Clone)]
pub struct RustAction {}
impl RustAction {
pub fn new() -> Self {
Self {}
}
fn execute_content(
&self,
version: &semver::Version,
cargo_content: &str,
) -> anyhow::Result<String> {
tracing::trace!("parsing Cargo.toml file as tolm");
let mut cargo_doc = cargo_content.parse::<toml_edit::DocumentMut>()?;
tracing::debug!(
"updating cargo workspace package version to {}",
version.to_string()
);
let workspace = if cargo_doc.contains_table("workspace") {
cargo_doc["workspace"].as_table_mut().unwrap()
} else {
let mut t = toml_edit::Table::new();
t.set_implicit(true);
cargo_doc["workspace"] = toml_edit::Item::Table(t);
cargo_doc["workspace"].as_table_mut().unwrap()
};
let package = workspace["package"].or_insert(toml_edit::table());
package["version"] = toml_edit::value(version.to_string());
Ok(cargo_doc.to_string())
}
}
impl Action for RustAction {
fn enabled(&self, config: &ActionConfig) -> anyhow::Result<bool> {
if let Ok(v) = std::env::var("CUDDLE_PLEASE_RUST_ACTION") {
if let Ok(true) = v.parse::<bool>() {
return Ok(true);
}
}
let val = &config[self.name().as_str()];
if val.is_badvalue() {
return Ok(false);
}
Ok(val.as_bool().unwrap_or(true))
}
fn name(&self) -> String {
"rust".into()
}
fn execute(&self, version: &semver::Version) -> anyhow::Result<()> {
tracing::info!(
"running rust action for version: {} and file: Cargo.toml",
version.to_string()
);
let path = std::path::PathBuf::from("Cargo.toml");
tracing::trace!("reading Cargo.toml");
let file = match std::fs::read_to_string(&path) {
Ok(file) => file,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => {
anyhow::bail!("err: Cargo.toml was not found in dir")
}
_ => Err(e)?,
},
};
let cargo_doc = self.execute_content(version, &file)?;
let mut cargo_file = std::fs::File::create(&path)?;
cargo_file.write_all(cargo_doc.as_bytes())?;
cargo_file.sync_all()?;
tracing::debug!("finished writing cargo file");
Ok(())
}
}
#[cfg(test)]
mod test {
use semver::{BuildMetadata, Prerelease};
use crate::{actions::Action, ActionConfig};
use super::RustAction;
#[test]
fn test_is_enabled() {
let config = ActionConfig::try_from(
r#"
please:
actions:
rust: true
"#,
)
.unwrap();
let enabled = RustAction::new().enabled(&config).unwrap();
assert!(enabled)
}
#[test]
fn test_is_disabled_by_default() {
let config = ActionConfig::try_from(
r#"
please:
"#,
)
.unwrap();
let enabled = RustAction::new().enabled(&config).unwrap();
assert!(!enabled)
}
#[test]
fn test_is_disabled() {
let config = ActionConfig::try_from(
r#"
please:
actions:
rust: false
"#,
)
.unwrap();
let enabled = RustAction::new().enabled(&config).unwrap();
assert!(!enabled)
}
#[test]
fn test_missing_value_is_enabled() {
let config = ActionConfig::try_from(
r#"
please:
actions:
rust:
"#,
)
.unwrap();
let enabled = RustAction::new().enabled(&config).unwrap();
assert!(enabled)
}
#[test]
fn test_can_edit_empty_file() {
let output = RustAction::default()
.execute_content(
&semver::Version {
major: 0,
minor: 1,
patch: 0,
pre: Prerelease::default(),
build: BuildMetadata::default(),
},
"",
)
.unwrap();
pretty_assertions::assert_eq!(
r#"[workspace.package]
version = "0.1.0"
"#,
&output,
)
}
#[test]
fn test_only_edits_stuff() {
let input = r#"
[workspace]
members = ["."]
[package]
something = {some = "something"}
# Some comment
[workspace.package]
version = "0.0.0" # some comment
readme = "../../"
"#;
let output = RustAction::default()
.execute_content(
&semver::Version {
major: 0,
minor: 1,
patch: 0,
pre: Prerelease::default(),
build: BuildMetadata::default(),
},
input,
)
.unwrap();
pretty_assertions::assert_eq!(
r#"
[workspace]
members = ["."]
[package]
something = {some = "something"}
# Some comment
[workspace.package]
version = "0.1.0"
readme = "../../"
"#,
&output,
)
}
}