use leon::Template;
use std::{io::Write, path::Path};
use crate::git;
use anyhow::Result;
use ptree::TreeBuilder;
use tracing::instrument;
pub const TEMPLATE_README: &str = include_str!("../etc/README.md");
#[allow(clippy::too_many_arguments)]
#[instrument(name = "workspace", skip(dir, name, description, dry, author, tree))]
pub fn create(
dir: &Path,
name: impl AsRef<str> + std::fmt::Display,
description: Option<impl AsRef<str> + std::fmt::Display>,
dry: bool,
no_readme_override: bool,
author: Option<Vec<String>>,
overrides: Option<Vec<String>>,
mut tree: Option<&mut TreeBuilder>,
) -> Result<()> {
tracing::info!("Creating top level workspace artifacts for {}", name);
let description =
description.map(|s| s.as_ref().to_string()).unwrap_or(format!("{} workspace", name));
if !dry && !no_readme_override {
tracing::debug!("Writing {:?}", dir.join("README.md"));
let templated_readme =
format_template_readme(name.as_ref(), &description, &get_current_username(&author))?;
let mut file = std::fs::File::create(dir.join("README.md"))?;
file.write_all(templated_readme.as_bytes())?;
}
if !no_readme_override {
tree.as_deref_mut().map(|t| t.add_empty_child("README.md".to_string()));
}
if !dry {
tracing::debug!("Writing {:?}", dir.join("Cargo.toml"));
fill_cargo(&dir.join("Cargo.toml"), author, name.as_ref(), &description, overrides)?;
}
tree.map(|t| t.add_empty_child("Cargo.toml".to_string()));
Ok(())
}
pub fn get_current_username(authors: &Option<Vec<String>>) -> String {
match authors {
Some(v) => v[0].clone(),
None => match git::try_git_username() {
Some(name) => name,
None => whoami::username().to_string(),
},
}
}
pub fn get_authors(authors: Option<Vec<String>>) -> toml_edit::Item {
let mut array = toml_edit::Array::default();
match authors {
Some(v) => v.into_iter().for_each(|a| array.push(a)),
None => match git::try_git_username() {
Some(name) => array.push(name),
None => array.push(whoami::username().to_string()),
},
};
toml_edit::value(array)
}
pub fn fetch_version(c: &str) -> Option<String> {
let cargo_search_output =
std::process::Command::new("cargo").arg("search").arg(c).output().ok()?;
if !cargo_search_output.status.success() {
tracing::warn!("Failed to run `cargo search {}` command", c);
return None;
}
let output_str = String::from_utf8(cargo_search_output.stdout).ok()?;
let anyhow_line = output_str.lines().find(|l| l.starts_with(&format!("{} = ", c)))?;
let version =
anyhow_line.strip_prefix(&format!("{} = \"", c)).and_then(|s| s.split('"').next());
version.map(|s| s.to_string())
}
pub fn fill_cargo(
file: &Path,
author: Option<Vec<String>>,
name: &str,
description: &str,
overrides: Option<Vec<String>>,
) -> Result<()> {
let mut manifest = toml_edit::Document::new();
manifest["workspace"] = toml_edit::Item::Table(toml_edit::Table::new());
let mut array = toml_edit::Array::default();
array.push("bin/*".to_string());
array.push("crates/*".to_string());
manifest["workspace"]["members"] = toml_edit::value(array);
manifest["workspace"]["resolver"] = toml_edit::value("2");
manifest["workspace.package"] = toml_edit::Item::Table(toml_edit::Table::new());
manifest["workspace.package"]["name"] = toml_edit::value(name);
manifest["workspace.package"]["description"] = toml_edit::value(description);
manifest["workspace.package"]["version"] = toml_edit::value("0.1.0");
manifest["workspace.package"]["edition"] = toml_edit::value("2021");
manifest["workspace.package"]["license"] = toml_edit::value("MIT");
let user = get_current_username(&author);
let repo = git::build_repository_url(&user, name);
manifest["workspace.package"]["authors"] = get_authors(author);
manifest["workspace.package"]["repository"] = toml_edit::value(&repo);
manifest["workspace.package"]["homepage"] = toml_edit::value(&repo);
let mut array = toml_edit::Array::default();
array.push("**/target".to_string());
array.push("benches/".to_string());
array.push("tests".to_string());
manifest["workspace.package"]["exclude"] = toml_edit::value(array);
add_workspace_deps(&mut manifest, overrides);
manifest["profile.dev"] = toml_edit::Item::Table(toml_edit::Table::new());
manifest["profile.dev"]["opt-level"] = toml_edit::value(1);
manifest["profile.dev"]["overflow-checks"] = toml_edit::value(false);
manifest["profile.bench"] = toml_edit::Item::Table(toml_edit::Table::new());
manifest["profile.bench"]["debug"] = toml_edit::value(true);
let mut file = std::fs::File::create(file)?;
let manifest_string = remove_table_quotes(manifest.to_string());
file.write_all(manifest_string.as_bytes())?;
Ok(())
}
pub fn list_dependencies() -> Result<()> {
let mut table = prettytable::Table::new();
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("Dependency"),
prettytable::Cell::new("Version"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("anyhow"),
prettytable::Cell::new("1.0"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("inquire"),
prettytable::Cell::new("0.6.2"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("tracing"),
prettytable::Cell::new("0.1.39"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("serde"),
prettytable::Cell::new("1.0.189"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("serde_json"),
prettytable::Cell::new("1.0.107"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("tracing-subscriber"),
prettytable::Cell::new("0.3.17"),
]));
table.add_row(prettytable::Row::new(vec![
prettytable::Cell::new("clap"),
prettytable::Cell::new("4.4.3"),
]));
table.printstd();
Ok(())
}
pub fn add_workspace_deps(manifest: &mut toml_edit::Document, overrides: Option<Vec<String>>) {
let default_inline_dependencies = vec![
("anyhow".to_string(), "1.0".to_string()),
("inquire".to_string(), "0.6.2".to_string()),
("tracing".to_string(), "0.1.39".to_string()),
("serde".to_string(), "1.0.189".to_string()),
("serde_json".to_string(), "1.0.107".to_string()),
("tracing-subscriber".to_string(), "0.3.17".to_string()),
("clap".to_string(), "4.4.3".to_string()),
];
let combined = match overrides {
Some(v) => {
let mut combined = default_inline_dependencies;
let override_deps = v.into_iter().map(|s| (s, "0.0.0".to_string()));
combined.extend(override_deps);
combined
}
None => default_inline_dependencies,
};
manifest["workspace.dependencies"] = toml_edit::Item::Table(toml_edit::Table::new());
add_inline_deps(manifest, combined);
manifest["workspace.dependencies"]["clap"] =
toml_edit::Item::Value(toml_edit::Value::InlineTable(toml_edit::InlineTable::new()));
let version = fetch_version("clap").unwrap_or_else(|| "4.4.3".to_string());
manifest["workspace.dependencies"]["clap"]["version"] = toml_edit::value(version);
let mut array = toml_edit::Array::default();
array.push("derive".to_string());
manifest["workspace.dependencies"]["clap"]["features"] = toml_edit::value(array);
}
pub fn add_inline_deps(manifest: &mut toml_edit::Document, deps: Vec<(String, String)>) {
let deps_table = manifest["workspace.dependencies"].as_table_mut().unwrap();
for (dep, default_version) in deps {
let version = fetch_version(&dep).unwrap_or_else(|| default_version.to_string());
deps_table[&dep] = toml_edit::value(version);
}
}
pub fn remove_table_quotes(s: String) -> String {
let re = regex::Regex::new(r#"\["(.*\..*)"\]"#).unwrap_or_else(|_| panic!("Invalid regex"));
let result = re.replace_all(&s, |caps: ®ex::Captures<'_>| format!("[{}]", &caps[1]));
result.to_string()
}
pub fn format_template_readme(
project_name: &str,
project_description: &str,
project_owner: &str,
) -> Result<String> {
let template = Template::parse(TEMPLATE_README)?;
let mut context = std::collections::HashMap::new();
context.insert("projectname", project_name);
context.insert("projectdescription", project_description);
context.insert("projectowner", project_owner);
let formatted = template.render(&context)?;
Ok(formatted)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs::File, io::Read};
use tempfile::tempdir;
#[test]
fn test_format_template_readme() {
assert!(TEMPLATE_README.contains("{projectname}"));
assert!(TEMPLATE_README.contains("{projectdescription}"));
assert!(TEMPLATE_README.contains("{projectowner}"));
let template_readme = format_template_readme("example", "example workspace", "refcell")
.unwrap_or_else(|_| panic!("Failed to format template readme"));
assert!(template_readme.contains("example"));
assert!(template_readme.contains("example workspace"));
assert!(template_readme.contains("refcell"));
}
#[test]
fn test_fetch_version() {
let version = fetch_version("anyhow").unwrap();
let expected = semver::Version::parse("1.0.75").unwrap();
let semversion = semver::Version::parse(&version).unwrap();
assert!(semversion.gt(&expected) || semversion.eq(&expected));
}
#[test]
fn test_remove_table_quotes() {
let s = r#"[workspace.package]"#;
let expected = r#"[workspace.package]"#;
assert_eq!(remove_table_quotes(s.to_string()), expected);
let s = r#"["workspace.package"]"#;
let expected = r#"[workspace.package]"#;
assert_eq!(remove_table_quotes(s.to_string()), expected);
let s = r#"["refcell"]"#;
let expected = r#"["refcell"]"#;
assert_eq!(remove_table_quotes(s.to_string()), expected);
let s = r#"["**/target", "benches/", "tests"]"#;
let expected = r#"["**/target", "benches/", "tests"]"#;
assert_eq!(remove_table_quotes(s.to_string()), expected);
let s = r#"["workspace.package.inside"]"#;
let expected = r#"[workspace.package.inside]"#;
assert_eq!(remove_table_quotes(s.to_string()), expected);
}
#[test]
fn test_fill_cargo() {
let dir = tempdir().unwrap();
let dir_path_buf = dir.path().to_path_buf();
let proj_name = "example";
let cargo_toml_path_buf = dir_path_buf.join("Cargo.toml");
fill_cargo(
&cargo_toml_path_buf,
Some(vec!["refcell".to_string()]),
proj_name,
"example workspace",
None,
)
.unwrap();
assert!(cargo_toml_path_buf.exists());
let mut cargo_toml = File::open(cargo_toml_path_buf).unwrap();
let mut cargo_toml_contents = String::new();
cargo_toml.read_to_string(&mut cargo_toml_contents).unwrap();
let anyhow_version = fetch_version("anyhow").unwrap_or_else(|| "1.0".to_string());
let inquire_version = fetch_version("inquire").unwrap_or_else(|| "0.6.2".to_string());
let tracing_version = fetch_version("tracing").unwrap_or_else(|| "0.1.39".to_string());
let serde_version = fetch_version("serde").unwrap_or_else(|| "1.0.189".to_string());
let serde_json_version =
fetch_version("serde_json").unwrap_or_else(|| "1.0.107".to_string());
let tracing_subscriber_version =
fetch_version("tracing-subscriber").unwrap_or_else(|| "0.3.17".to_string());
let clap_version = fetch_version("clap").unwrap_or_else(|| "4.4.3".to_string());
let expected_contents = format!(
r#"[workspace]
members = ["bin/*", "crates/*"]
resolver = "2"
[workspace.package]
name = "example"
description = "example workspace"
version = "0.1.0"
edition = "2021"
license = "MIT"
authors = ["refcell"]
repository = "https://github.com/refcell/example"
homepage = "https://github.com/refcell/example"
exclude = ["**/target", "benches/", "tests"]
[workspace.dependencies]
anyhow = "{}"
inquire = "{}"
tracing = "{}"
serde = "{}"
serde_json = "{}"
tracing-subscriber = "{}"
clap = {{ version = "{}", features = ["derive"] }}
[profile.dev]
opt-level = 1
overflow-checks = false
[profile.bench]
debug = true
"#,
anyhow_version,
inquire_version,
tracing_version,
serde_version,
serde_json_version,
tracing_subscriber_version,
clap_version
);
assert_eq!(cargo_toml_contents, expected_contents);
}
#[test]
fn test_create() {
let dir = tempdir().unwrap();
let dir_path_buf = dir.path().to_path_buf();
create(&dir_path_buf, "example", Some("example workspace"), false, false, None, None, None)
.unwrap();
assert!(dir_path_buf.exists());
assert!(dir_path_buf.join("Cargo.toml").exists());
assert!(dir_path_buf.join("README.md").exists());
}
#[test]
fn test_create_dry_run() {
let dir = tempdir().unwrap();
let dir_path_buf = dir.path().to_path_buf();
create(&dir_path_buf, "example", Some("example workspace"), true, false, None, None, None)
.unwrap();
assert!(!dir_path_buf.join("Cargo.toml").exists());
assert!(!dir_path_buf.join("README.md").exists());
}
}