use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use anyhow::{Context as _, Result};
use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, value};
use crate::backup;
use crate::cli::RuntimeChoice;
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Edits {
pub bin_names: Vec<String>,
pub had_src_conversion: bool,
pub has_lib_rs: bool,
pub needs: Needs,
pub runtimes: BTreeSet<RuntimeChoice>,
pub tests_integration: Vec<IntegrationTestEntry>,
pub workspace_dep_names: BTreeSet<String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct IntegrationTestEntry {
pub name: String,
pub path: String,
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct Needs {
pub anyhow: bool,
pub rudzio_test_cfg: bool,
}
#[inline]
pub fn apply(manifest_path: &Path, edits: &Edits) -> Result<bool> {
let source = fs::read_to_string(manifest_path)
.with_context(|| format!("reading {}", manifest_path.display()))?;
let mut doc: DocumentMut = source
.parse()
.with_context(|| format!("parsing {}", manifest_path.display()))?;
let before = doc.to_string();
if edits.had_src_conversion {
set_autotests_false(&mut doc);
if edits.has_lib_rs {
set_lib_harness_false(&mut doc);
}
}
if edits.has_lib_rs {
set_lib_test_false(&mut doc);
}
for name in &edits.bin_names {
set_bin_test_false(&mut doc, name);
}
if !edits.runtimes.is_empty() {
set_rudzio_dependency(
&mut doc,
&edits.runtimes,
edits.workspace_dep_names.contains("rudzio"),
);
}
if edits.needs.anyhow {
set_anyhow_dependency(&mut doc, edits.workspace_dep_names.contains("anyhow"));
}
for entry in &edits.tests_integration {
ensure_test_entry(&mut doc, entry);
}
if edits.needs.rudzio_test_cfg {
ensure_check_cfg_rudzio_test(&mut doc);
}
let after = doc.to_string();
if before == after {
return Ok(false);
}
let _backup = backup::copy_before_write(manifest_path)
.with_context(|| format!("backing up {}", manifest_path.display()))?;
fs::write(manifest_path, &after)
.with_context(|| format!("writing {}", manifest_path.display()))?;
Ok(true)
}
fn dep_already_present(doc: &DocumentMut, name: &str) -> bool {
for section in ["dependencies", "dev-dependencies"] {
if let Some(tbl) = doc.as_table().get(section).and_then(Item::as_table)
&& tbl.contains_key(name)
{
return true;
}
}
false
}
fn ensure_check_cfg_rudzio_test(doc: &mut DocumentMut) {
const CFG_ENTRY: &str = "cfg(rudzio_test)";
let lints_was_absent = !doc.as_table().contains_key("lints");
let lints = doc
.as_table_mut()
.entry("lints")
.or_insert(Item::Table(Table::new()));
let Some(lints_tbl) = lints.as_table_mut() else {
return;
};
if lints_was_absent {
lints_tbl.set_implicit(true);
}
let rust = lints_tbl.entry("rust").or_insert(Item::Table(Table::new()));
let Some(rust_tbl) = rust.as_table_mut() else {
return;
};
let existed = rust_tbl.contains_key("unexpected_cfgs");
let unexpected =
rust_tbl
.entry("unexpected_cfgs")
.or_insert(Item::Value(toml_edit::Value::InlineTable({
let mut table = InlineTable::new();
let _lvl = table.insert("level", "warn".into());
let mut arr = Array::new();
arr.push(CFG_ENTRY);
let _cc = table.insert("check-cfg", arr.into());
table
})));
if !existed {
return;
}
let Item::Value(toml_edit::Value::InlineTable(inline)) = unexpected else {
return;
};
let check_cfg_item = inline
.entry("check-cfg")
.or_insert(toml_edit::Value::Array(Array::new()));
if let toml_edit::Value::Array(arr) = check_cfg_item {
let already = arr
.iter()
.any(|item| item.as_str().is_some_and(|text| text == CFG_ENTRY));
if !already {
arr.push(CFG_ENTRY);
}
}
}
fn ensure_test_entry(doc: &mut DocumentMut, entry: &IntegrationTestEntry) {
let tests_item = doc
.as_table_mut()
.entry("test")
.or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
let Some(arr) = tests_item.as_array_of_tables_mut() else {
return;
};
for existing in arr.iter_mut() {
let name_match = existing
.get("name")
.and_then(Item::as_str)
.is_some_and(|text| text == entry.name);
let path_match = existing
.get("path")
.and_then(Item::as_str)
.is_some_and(|text| text == entry.path);
if name_match || path_match {
let harness_is_false = existing
.get("harness")
.and_then(Item::as_bool)
.is_some_and(|flag| !flag);
if !harness_is_false {
let _prev = existing.insert("harness", value(false));
}
return;
}
}
let mut tbl = Table::new();
let _prev_name = tbl.insert("name", value(entry.name.clone()));
let _prev_path = tbl.insert("path", value(entry.path.clone()));
let _prev_harness = tbl.insert("harness", value(false));
arr.push(tbl);
}
fn set_anyhow_dependency(doc: &mut DocumentMut, workspace_pins_anyhow: bool) {
if dep_already_present(doc, "anyhow") {
return;
}
let entry = if workspace_pins_anyhow {
let mut tbl = InlineTable::new();
let _w = tbl.insert("workspace", true.into());
Item::Value(tbl.into())
} else {
value("1.0")
};
let dev_deps = doc
.as_table_mut()
.entry("dev-dependencies")
.or_insert(Item::Table(Table::new()));
let Some(dev_tbl) = dev_deps.as_table_mut() else {
return;
};
let _prev = dev_tbl.insert("anyhow", entry);
}
fn set_autotests_false(doc: &mut DocumentMut) {
let package = doc
.as_table_mut()
.entry("package")
.or_insert(Item::Table(Table::new()));
let Some(pkg) = package.as_table_mut() else {
return;
};
let _prev = pkg.insert("autotests", value(false));
}
fn set_bin_test_false(doc: &mut DocumentMut, bin_name: &str) {
let bins_item = doc
.as_table_mut()
.entry("bin")
.or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
let Some(arr) = bins_item.as_array_of_tables_mut() else {
return;
};
for existing in arr.iter_mut() {
let name_match = existing
.get("name")
.and_then(Item::as_str)
.is_some_and(|text| text == bin_name);
if name_match {
let already_false = existing
.get("test")
.and_then(Item::as_value)
.and_then(toml_edit::Value::as_bool)
.is_some_and(|flag| !flag);
if !already_false {
let _prev = existing.insert("test", value(false));
}
return;
}
}
let mut tbl = Table::new();
let _prev_name = tbl.insert("name", value(bin_name.to_owned()));
let _prev_test = tbl.insert("test", value(false));
arr.push(tbl);
}
fn set_lib_harness_false(doc: &mut DocumentMut) {
let lib = doc
.as_table_mut()
.entry("lib")
.or_insert(Item::Table(Table::new()));
let Some(lib_tbl) = lib.as_table_mut() else {
return;
};
let user_opted_in = lib_tbl
.get("harness")
.and_then(Item::as_value)
.and_then(toml_edit::Value::as_bool)
.is_some_and(|flag| flag);
if user_opted_in {
return;
}
let _prev = lib_tbl.insert("harness", value(false));
}
fn set_lib_test_false(doc: &mut DocumentMut) {
let lib = doc
.as_table_mut()
.entry("lib")
.or_insert(Item::Table(Table::new()));
let Some(lib_tbl) = lib.as_table_mut() else {
return;
};
let user_opted_in = lib_tbl
.get("test")
.and_then(Item::as_value)
.and_then(toml_edit::Value::as_bool)
.is_some_and(|flag| flag);
if user_opted_in {
return;
}
let _prev = lib_tbl.insert("test", value(false));
}
fn set_rudzio_dependency(
doc: &mut DocumentMut,
runtimes: &BTreeSet<RuntimeChoice>,
workspace_pins_rudzio: bool,
) {
if dep_already_present(doc, "rudzio") {
return;
}
let features = {
let mut arr = Array::new();
arr.push("common");
let mut feat_set: BTreeSet<&'static str> = BTreeSet::new();
for runtime in runtimes {
let _inserted = feat_set.insert(runtime.cargo_feature());
}
if feat_set.is_empty() {
let _inserted = feat_set.insert(RuntimeChoice::TokioMt.cargo_feature());
}
for feat in feat_set {
arr.push(feat);
}
arr
};
let mut table = InlineTable::new();
if workspace_pins_rudzio {
let _w = table.insert("workspace", true.into());
} else {
let _v = table.insert("version", "0.1".into());
}
let _f = table.insert("features", toml_edit::Value::from(features));
let dev_deps = doc
.as_table_mut()
.entry("dev-dependencies")
.or_insert(Item::Table(Table::new()));
let Some(dev_tbl) = dev_deps.as_table_mut() else {
return;
};
let _prev = dev_tbl.insert("rudzio", Item::Value(table.into()));
}