use std::path::{Path, PathBuf};
use anyhow::Result;
pub fn resolve_added_dir(base: &Path, raw: &str) -> Result<PathBuf, String> {
let raw = raw.trim();
if raw.is_empty() {
return Err("an added directory needs a path".to_string());
}
let expanded = expand_home(raw);
let candidate = if expanded.is_absolute() {
expanded
} else {
base.join(expanded)
};
let real = std::fs::canonicalize(&candidate)
.map_err(|e| format!("cannot add directory '{raw}': {e}"))?;
if !real.is_dir() {
return Err(format!(
"cannot add directory '{raw}': {} is not a directory",
real.display()
));
}
Ok(real)
}
pub fn apply_added_dirs(
config: &mut mermaid_domain::Config,
project_dir: &Path,
flags: &[PathBuf],
) -> Result<()> {
let mut resolved: Vec<PathBuf> = Vec::new();
for raw in &config.workspace.additional_dirs {
let dir = resolve_added_dir(project_dir, &raw.to_string_lossy())
.map_err(|e| anyhow::anyhow!("[workspace] additional_dirs: {e}"))?;
push_unique(&mut resolved, dir);
}
let shell_cwd = std::env::current_dir()?;
for raw in flags {
let dir = resolve_added_dir(&shell_cwd, &raw.to_string_lossy())
.map_err(|e| anyhow::anyhow!("--add-dir: {e}"))?;
push_unique(&mut resolved, dir);
}
config.workspace.additional_dirs = resolved;
Ok(())
}
fn push_unique(dirs: &mut Vec<PathBuf>, dir: PathBuf) {
if !dirs.contains(&dir) {
dirs.push(dir);
}
}
fn expand_home(raw: &str) -> PathBuf {
let rest = match raw.strip_prefix('~') {
Some("") => Some(""),
Some(rest) if rest.starts_with('/') || rest.starts_with('\\') => Some(&rest[1..]),
_ => None,
};
match (rest, directories::BaseDirs::new()) {
(Some(rest), Some(dirs)) => dirs.home_dir().join(rest),
_ => PathBuf::from(raw),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_base(tag: &str) -> PathBuf {
let base = std::env::temp_dir().join(format!("mermaid_added_{tag}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
base
}
#[test]
fn resolves_relative_paths_against_the_base_and_canonicalizes() {
let base = temp_base("rel");
std::fs::create_dir_all(base.join("project")).unwrap();
std::fs::create_dir_all(base.join("lib")).unwrap();
let got = resolve_added_dir(&base.join("project"), "../lib").unwrap();
assert_eq!(got, std::fs::canonicalize(base.join("lib")).unwrap());
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn missing_paths_and_files_are_errors_naming_the_path() {
let base = temp_base("missing");
let err = resolve_added_dir(&base, "nope").unwrap_err();
assert!(err.contains("'nope'"), "{err}");
std::fs::write(base.join("file.txt"), "x").unwrap();
let err = resolve_added_dir(&base, "file.txt").unwrap_err();
assert!(err.contains("not a directory"), "{err}");
assert!(resolve_added_dir(&base, " ").is_err());
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn apply_canonicalizes_dedupes_and_fails_on_a_missing_entry() {
let base = temp_base("apply");
std::fs::create_dir_all(base.join("project")).unwrap();
std::fs::create_dir_all(base.join("lib")).unwrap();
let lib = std::fs::canonicalize(base.join("lib")).unwrap();
let mut config = mermaid_domain::Config::default();
config.workspace.additional_dirs = vec![PathBuf::from("../lib")];
apply_added_dirs(
&mut config,
&base.join("project"),
std::slice::from_ref(&lib),
)
.unwrap();
assert_eq!(config.workspace.additional_dirs, vec![lib]);
let mut config = mermaid_domain::Config::default();
let err = apply_added_dirs(
&mut config,
&base.join("project"),
&[base.join("does-not-exist")],
)
.unwrap_err();
assert!(err.to_string().starts_with("--add-dir:"), "{err}");
let _ = std::fs::remove_dir_all(&base);
}
}