use super::*;
pub(super) fn plan(root: &Path, old_path: &str, new_path: &str, old: &str, new: &str) -> Result<Vec<(PathBuf, String)>> {
let workspace: Value = toml::from_str(&read_to_string(root.join("Cargo.toml"))?)?;
let members = workspace["workspace"]["members"].as_array().context("Expected workspace members")?;
let old_dir = normalize_path(root.join(old_path));
let new_dir = normalize_path(root.join(new_path));
let mut edits = std::collections::BTreeMap::new();
for dir in std::iter::once(root.to_path_buf()).chain(members.iter().filter_map(Value::as_str).map(|member| root.join(member))) {
let path = dir.join("Cargo.toml");
let mut cargo: Value = toml::from_str(&read_to_string(&path).with_context(|| format!("Read library consumer {}", path.display()))?)?;
let mut namespace = false;
if update(&mut cargo, &dir, &old_dir, &new_dir, old, new, &mut namespace)? {
let output = path.strip_prefix(&old_dir).map(|tail| new_dir.join(tail)).unwrap_or(path);
edits.insert(output, toml::to_string_pretty(&cargo)?);
if namespace && dir != root {
sources(&dir, &old_dir, &new_dir, old, new, &mut edits)?;
}
}
}
Ok(edits.into_iter().collect())
}
fn update(value: &mut Value, dir: &Path, old_dir: &Path, new_dir: &Path, old: &str, new: &str, namespace: &mut bool) -> Result<bool> {
let Some(table) = value.as_table_mut() else { return Ok(false); };
let mut changed = false;
for (section, value) in table.iter_mut() {
if matches!(section.as_str(), "dependencies" | "dev-dependencies" | "build-dependencies") {
let Some(deps) = value.as_table_mut() else { continue; };
let keys: Vec<_> = deps.iter().filter(|(_, dep)| dependency_path(dep).is_some_and(|path| normalize_path(dir.join(path)) == old_dir)).map(|(name, _)| name.clone()).collect();
for key in keys {
let renamed = key == old;
anyhow::ensure!(!renamed || !deps.contains_key(new), "Library rename would overwrite dependency {new} in {}; choose an explicit alias first", dir.display());
let mut dep = deps.remove(&key).context("Expected dependency")?;
let dep_table = dep.as_table_mut().context("Expected path dependency")?;
let path = pathdiff::diff_paths(new_dir, dir).context("Library relative path")?;
dep_table.insert("path".into(), Value::String(path.to_string_lossy().into_owned()));
if !renamed || dep_table.contains_key("package") {
dep_table.insert("package".into(), Value::String(new.into()));
}
deps.insert(if renamed { new.into() } else { key }, dep);
*namespace |= renamed;
changed = true;
}
} else {
changed |= update(value, dir, old_dir, new_dir, old, new, namespace)?;
}
}
Ok(changed)
}
fn sources(dir: &Path, old_dir: &Path, new_dir: &Path, old: &str, new: &str, edits: &mut std::collections::BTreeMap<PathBuf, String>) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if entry.file_type()?.is_symlink() { continue; }
if path.is_dir() {
if !matches!(entry.file_name().to_str(), Some("target" | "node_modules" | ".git")) {
sources(&path, old_dir, new_dir, old, new, edits)?;
}
} else if path.extension().is_some_and(|ext| ext == "rs") {
let original = read_to_string(&path)?;
let changed = native_layout::replace_namespace(&original, &old.to_case(Case::Snake), &new.to_case(Case::Snake));
if changed != original {
let output = path.strip_prefix(old_dir).map(|tail| new_dir.join(tail)).unwrap_or(path);
edits.insert(output, changed);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alias_and_dependency_flags_survive_library_rename() {
let mut value: Value = toml::from_str("[dependencies]\nshared={path='../core/shared', default-features=false, features=['custom']}\n[build-dependencies]\nmodels={package='shared', path='../core/shared', optional=true}\n").unwrap();
let mut namespace = false;
assert!(update(&mut value, Path::new("/app/src/api"), Path::new("/app/src/core/shared"), Path::new("/app/src/core/common-core"), "shared", "common-core", &mut namespace).unwrap());
assert!(namespace);
assert!(value["dependencies"].get("shared").is_none());
assert_eq!(value["dependencies"]["common-core"]["path"].as_str(), Some("../core/common-core"));
assert_eq!(value["dependencies"]["common-core"]["default-features"].as_bool(), Some(false));
assert_eq!(value["dependencies"]["common-core"]["features"][0].as_str(), Some("custom"));
assert_eq!(value["build-dependencies"]["models"]["package"].as_str(), Some("common-core"));
assert_eq!(value["build-dependencies"]["models"]["optional"].as_bool(), Some(true));
}
}