use anyhow::{Result, bail};
mod cargo_toml_constants;
pub(crate) use cargo_toml_constants::*;
const DEPENDENCY_TABLES: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
pub(crate) fn version(name: &str) -> Option<&'static str> {
CRATE_VERSIONS
.iter()
.find(|(crate_name, _)| *crate_name == name)
.map(|(_, version)| *version)
}
pub(crate) fn fill(source: &str) -> Result<String> {
let Ok(mut document) = source.parse::<toml_edit::DocumentMut>() else { return Ok(source.into()) };
let mut changed = false;
let mut tables: Vec<&mut toml_edit::Item> = Vec::new();
let root = document.as_table_mut();
let (workspace, rest): (Vec<_>, Vec<_>) =
root.iter_mut().partition(|(key, _)| key.get() == "workspace");
for (key, item) in rest {
if DEPENDENCY_TABLES.contains(&key.get()) {
tables.push(item);
} else if key.get() == "target" {
for (_, target) in item.as_table_like_mut().into_iter().flat_map(|table| table.iter_mut()) {
for (key, item) in target.as_table_like_mut().into_iter().flat_map(|table| table.iter_mut()) {
if DEPENDENCY_TABLES.contains(&key.get()) {
tables.push(item);
}
}
}
}
}
for (_, item) in workspace {
if let Some(item) = item.as_table_like_mut().and_then(|table| table.get_mut("dependencies")) {
tables.push(item);
}
}
for table in tables {
let Some(table) = table.as_table_like_mut() else { continue };
for (name, dependency) in table.iter_mut() {
changed |= fill_dependency(name.get(), dependency)?;
}
}
Ok(if changed { document.to_string() } else { source.into() })
}
fn fill_dependency(name: &str, dependency: &mut toml_edit::Item) -> Result<bool> {
let Some(spec) = dependency.as_table_like() else { return Ok(false) };
if ["version", "path", "git", "workspace"].iter().any(|key| spec.contains_key(key)) {
return Ok(false);
}
let Some(version) = version(name) else {
bail!("Cargo dependency `{name}` has no version and the CLI has no default for it");
};
if spec.is_empty() {
*dependency = toml_edit::value(version);
return Ok(true);
}
let mut inline = toml_edit::InlineTable::new();
inline.insert("version", version.into());
for (key, value) in spec.iter() {
if let Some(value) = value.as_value() {
inline.insert(key, value.clone());
}
}
*dependency = toml_edit::value(inline);
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn versionless_dependencies_take_registry_versions() {
let source = "[dependencies]\nserde_json = {}\ntokio = { features = [\"net\"] }\nlocal = { path = \"../local\" }\nuuid = \"1\"\n\n[target.'cfg(unix)'.dev-dependencies]\naxum = {}\n";
let filled = fill(source).unwrap();
let manifest: toml::Value = toml::from_str(&filled).unwrap();
assert_eq!(manifest["dependencies"]["serde_json"].as_str(), version("serde_json"));
assert_eq!(manifest["dependencies"]["tokio"]["version"].as_str(), version("tokio"));
assert_eq!(manifest["dependencies"]["tokio"]["features"][0].as_str(), Some("net"));
assert!(manifest["dependencies"]["local"].get("version").is_none());
assert_eq!(manifest["target"]["cfg(unix)"]["dev-dependencies"]["axum"].as_str(), version("axum"));
assert!(filled.contains("tokio = { version = "), "version leads the inline table: {filled}");
assert_eq!(fill(&filled).unwrap(), filled);
}
#[test]
fn unknown_versionless_dependency_is_an_error() {
let error = fill("[dependencies]\nleft-pad = {}\n").unwrap_err();
assert!(error.to_string().contains("left-pad"));
}
#[test]
fn templates_leave_versions_to_the_registry() {
let templates = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/templates/rust");
let mut checked = 0;
for kind in ["application", "billing", "cac", "ecommerce", "iam", "project"] {
let path = templates.join(kind).join("Cargo.toml.template");
let source = std::fs::read_to_string(&path).unwrap();
let mut section = String::new();
for line in source.lines().map(str::trim) {
if line.starts_with('[') {
section = line.trim_matches(['[', ']']).to_owned();
continue;
}
let Some((name, spec)) = line.split_once('=') else { continue };
let (name, spec) = (name.trim(), spec.trim());
if !DEPENDENCY_TABLES.iter().any(|table| section.ends_with(table)) || spec.contains("path") {
continue;
}
assert!(!spec.starts_with('"') && !spec.contains("version"), "{name} in {} pins its own version", path.display());
assert!(version(name).is_some(), "{name} in {} has no registry version", path.display());
checked += 1;
}
}
assert!(checked > 0, "No template dependencies were checked");
}
}