use std::path::{Path, PathBuf};
use crate::error::ClapfigError;
use crate::types::{Boundary, SearchPath};
pub fn resolve_search_path(
sp: &SearchPath,
app_name: &str,
cwd_override: Option<&Path>,
) -> Option<PathBuf> {
match sp {
SearchPath::Platform => {
let proj = directories::ProjectDirs::from("", "", app_name)?;
Some(proj.config_dir().to_path_buf())
}
SearchPath::Home(subdir) => {
let user = directories::UserDirs::new()?;
Some(user.home_dir().join(subdir))
}
SearchPath::Cwd => match cwd_override {
Some(dir) => Some(dir.to_path_buf()),
None => std::env::current_dir().ok(),
},
SearchPath::Path(p) => Some(p.clone()),
SearchPath::Ancestors(_) => {
panic!("resolve_search_path called with Ancestors — use expand_ancestors_from instead")
}
}
}
pub fn expand_ancestors_from(start: PathBuf, boundary: &Boundary) -> Vec<PathBuf> {
let mut dirs = Vec::new();
let mut current = start.as_path();
loop {
dirs.push(current.to_path_buf());
if let Boundary::Marker(name) = boundary
&& current.join(name).exists()
{
break;
}
match current.parent() {
Some(parent) => current = parent,
None => break, }
}
dirs.reverse();
dirs
}
pub fn expand_search_paths(
search_paths: &[SearchPath],
app_name: &str,
start_dir: &Path,
) -> Vec<PathBuf> {
let mut dirs = Vec::new();
for sp in search_paths {
match sp {
SearchPath::Ancestors(boundary) => {
dirs.extend(expand_ancestors_from(start_dir.to_path_buf(), boundary));
}
other => {
if let Some(dir) = resolve_search_path(other, app_name, Some(start_dir)) {
dirs.push(dir);
}
}
}
}
dirs
}
pub fn resolve_persist_path(
persist: &SearchPath,
file_name: &str,
app_name: &str,
) -> Result<PathBuf, ClapfigError> {
match persist {
SearchPath::Ancestors(_) => Err(ClapfigError::AncestorsNotAllowedAsPersistPath),
other => resolve_search_path(other, app_name, None)
.map(|dir| dir.join(file_name))
.ok_or(ClapfigError::NoPersistPath),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn resolve_explicit_path() {
let p = PathBuf::from("/tmp/myapp");
let resolved = resolve_search_path(&SearchPath::Path(p.clone()), "ignored", None);
assert_eq!(resolved, Some(p));
}
#[test]
fn resolve_cwd_uses_override_when_provided() {
let tmp = TempDir::new().unwrap();
let resolved = resolve_search_path(&SearchPath::Cwd, "ignored", Some(tmp.path()));
assert_eq!(resolved.as_deref(), Some(tmp.path()));
}
#[test]
fn resolve_cwd_falls_back_to_env_current_dir() {
let resolved = resolve_search_path(&SearchPath::Cwd, "ignored", None);
assert_eq!(resolved, std::env::current_dir().ok());
}
#[test]
fn expand_ancestors_root_includes_cwd() {
let cwd = std::env::current_dir().unwrap();
let dirs = expand_ancestors_from(cwd.clone(), &Boundary::Root);
assert!(!dirs.is_empty());
assert_eq!(dirs.last().unwrap(), &cwd);
}
#[test]
fn expand_ancestors_root_is_shallowest_first() {
let cwd = std::env::current_dir().unwrap();
let dirs = expand_ancestors_from(cwd, &Boundary::Root);
assert!(dirs.len() >= 2);
for pair in dirs.windows(2) {
assert!(
pair[1].starts_with(&pair[0]),
"{:?} should start with {:?}",
pair[1],
pair[0]
);
}
}
#[test]
fn expand_ancestors_marker_stops_at_marker() {
let dir = TempDir::new().unwrap();
let deep = dir.path().join("a").join("b").join("c");
fs::create_dir_all(&deep).unwrap();
fs::create_dir(dir.path().join("a").join(".git")).unwrap();
let dirs = expand_ancestors_from(deep.clone(), &Boundary::Marker(".git"));
assert!(dirs.contains(&dir.path().join("a")));
assert!(dirs.contains(&dir.path().join("a").join("b")));
assert!(dirs.contains(&dir.path().join("a").join("b").join("c")));
assert!(!dirs.contains(&dir.path().to_path_buf()));
}
#[test]
fn expand_ancestors_marker_missing_walks_to_root() {
let dir = TempDir::new().unwrap();
let deep = dir.path().join("x").join("y");
fs::create_dir_all(&deep).unwrap();
let dirs = expand_ancestors_from(deep.clone(), &Boundary::Marker(".nonexistent"));
assert!(dirs.contains(&dir.path().to_path_buf()));
assert!(dirs.contains(&deep));
}
#[test]
fn expand_search_paths_mixes_single_and_ancestors() {
let dir = TempDir::new().unwrap();
let deep = dir.path().join("a").join("b");
fs::create_dir_all(&deep).unwrap();
fs::create_dir(dir.path().join("a").join(".marker")).unwrap();
let explicit = TempDir::new().unwrap();
let paths = vec![
SearchPath::Path(explicit.path().to_path_buf()),
SearchPath::Ancestors(Boundary::Marker(".marker")),
];
let dirs = expand_search_paths(&paths, "test", &deep);
assert_eq!(dirs[0], explicit.path().to_path_buf());
assert!(dirs.contains(&dir.path().join("a")));
assert!(dirs.contains(&dir.path().join("a").join("b")));
let pos_a = dirs
.iter()
.position(|d| d == &dir.path().join("a"))
.unwrap();
let pos_ab = dirs
.iter()
.position(|d| d == &dir.path().join("a").join("b"))
.unwrap();
assert!(pos_ab > pos_a);
}
#[test]
fn persist_path_explicit() {
let p = PathBuf::from("/tmp/configs");
let result = resolve_persist_path(&SearchPath::Path(p.clone()), "app.toml", "test");
assert_eq!(result.unwrap(), p.join("app.toml"));
}
#[test]
fn persist_path_rejects_ancestors() {
let result =
resolve_persist_path(&SearchPath::Ancestors(Boundary::Root), "app.toml", "test");
assert!(matches!(
result,
Err(ClapfigError::AncestorsNotAllowedAsPersistPath)
));
}
}