use std::path::{Path, PathBuf};
use crate::error::Result;
use crate::registry::Registry;
pub const CONFIG_FILE_NAME: &str = "project-types.yaml";
pub const CONFIG_DIR_NAME: &str = "file-search-on";
pub const PER_PROJECT_DIR_NAME: &str = ".file-search-on";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveryEntry {
pub scope: &'static str,
pub path: PathBuf,
}
pub fn discovery_entries() -> Vec<DiscoveryEntry> {
entries_from(dirs::config_dir(), std::env::current_dir().ok())
}
pub fn discovery_paths() -> Vec<PathBuf> {
discovery_entries().into_iter().map(|e| e.path).collect()
}
fn entries_from(user_config_dir: Option<PathBuf>, cwd: Option<PathBuf>) -> Vec<DiscoveryEntry> {
let mut out = Vec::with_capacity(2);
if let Some(base) = user_config_dir {
out.push(DiscoveryEntry {
scope: "user-wide",
path: base.join(CONFIG_DIR_NAME).join(CONFIG_FILE_NAME),
});
}
if let Some(cwd) = cwd {
out.push(DiscoveryEntry {
scope: "per-project",
path: cwd.join(PER_PROJECT_DIR_NAME).join(CONFIG_FILE_NAME),
});
}
out
}
impl Registry {
pub fn load_discovered(&mut self) -> Result<usize> {
self.load_paths(&discovery_paths())
}
fn load_paths(&mut self, paths: &[PathBuf]) -> Result<usize> {
let mut total = 0;
for path in paths {
if Path::new(path).is_file() {
total += self.load_from_file(path)?;
}
}
Ok(total)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entries_order_user_wide_then_cwd() {
let entries = entries_from(
Some(PathBuf::from("/home/u/.config")),
Some(PathBuf::from("/work/proj")),
);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].scope, "user-wide");
assert!(entries[0]
.path
.ends_with(format!("{CONFIG_DIR_NAME}/{CONFIG_FILE_NAME}")));
assert_eq!(entries[1].scope, "per-project");
assert!(entries[1]
.path
.ends_with(format!("{PER_PROJECT_DIR_NAME}/{CONFIG_FILE_NAME}")));
}
#[test]
fn entries_omit_unresolvable_anchors() {
assert!(entries_from(None, None).is_empty());
assert_eq!(entries_from(Some(PathBuf::from("/x")), None).len(), 1);
assert_eq!(entries_from(None, Some(PathBuf::from("/y"))).len(), 1);
}
fn write(path: &Path, body: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
#[test]
fn load_paths_layers_both_configs() {
let tmp = tempfile::tempdir().unwrap();
let user = tmp.path().join("user/project-types.yaml");
let proj = tmp.path().join("proj/project-types.yaml");
write(
&user,
"project_types:\n - name: user-wide-app\n indicators:\n - has_file: user.marker\n",
);
write(
&proj,
"project_types:\n - name: project-local-app\n indicators:\n - has_file: local.marker\n",
);
let mut reg = Registry::new();
let n = reg.load_paths(&[user, proj]).unwrap();
assert_eq!(n, 2);
let names: Vec<&str> = reg.types().iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"user-wide-app"));
assert!(names.contains(&"project-local-app"));
}
#[test]
fn load_paths_no_configs_is_ok() {
let mut reg = Registry::new();
let missing = PathBuf::from("/definitely/not/here/project-types.yaml");
assert_eq!(reg.load_paths(&[missing]).unwrap(), 0);
}
#[test]
fn load_paths_bad_config_surfaces() {
let tmp = tempfile::tempdir().unwrap();
let bad = tmp.path().join("bad/project-types.yaml");
write(&bad, "project_types:\n - name: broken\n");
let mut reg = Registry::new();
assert!(reg.load_paths(&[bad]).is_err());
}
}