Skip to main content

elph_agent/
paths.rs

1//! Generic home, data, and project directory resolution.
2
3use std::path::{Path, PathBuf};
4
5/// Environment and naming knobs for an application layout.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct PathResolver {
8    /// Override env var for the config/home directory (e.g. `ELPH_HOME`).
9    pub home_env: &'static str,
10    /// Override env var for the data directory (e.g. `ELPH_DATA_DIR`).
11    pub data_env: &'static str,
12    /// Override env var for the project directory (e.g. `ELPH_PROJECT_DIR`).
13    pub project_env: &'static str,
14    /// Config directory name under `$HOME` (e.g. `.elph`).
15    pub config_dir_name: &'static str,
16    /// Data directory name under XDG data home (e.g. `elph`).
17    pub data_dir_name: &'static str,
18}
19
20/// Resolved config, data, and project directories.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ResolvedPaths {
23    pub config_dir: PathBuf,
24    pub data_dir: PathBuf,
25    pub project_dir: PathBuf,
26}
27
28impl PathResolver {
29    pub fn resolve(&self) -> std::io::Result<ResolvedPaths> {
30        Ok(ResolvedPaths::from_dirs(
31            self.config_dir()?,
32            self.data_dir()?,
33            self.project_dir()?,
34        ))
35    }
36
37    fn config_dir(&self) -> std::io::Result<PathBuf> {
38        if let Some(path) = env_path(self.home_env) {
39            return Ok(path);
40        }
41
42        Ok(user_home()?.join(self.config_dir_name))
43    }
44
45    fn data_dir(&self) -> std::io::Result<PathBuf> {
46        if let Some(path) = env_path(self.data_env) {
47            return Ok(path);
48        }
49
50        if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
51            let trimmed = xdg.trim();
52            if !trimmed.is_empty() {
53                return Ok(Path::new(trimmed).join(self.data_dir_name));
54            }
55        }
56
57        Ok(user_home()?.join(".local").join("share").join(self.data_dir_name))
58    }
59
60    fn project_dir(&self) -> std::io::Result<PathBuf> {
61        if let Some(path) = env_path(self.project_env) {
62            return Ok(path);
63        }
64
65        std::env::current_dir()
66    }
67}
68
69impl ResolvedPaths {
70    pub fn from_dirs(config_dir: PathBuf, data_dir: PathBuf, project_dir: PathBuf) -> Self {
71        Self {
72            config_dir,
73            data_dir,
74            project_dir,
75        }
76    }
77}
78
79fn env_path(name: &str) -> Option<PathBuf> {
80    let value = std::env::var(name).ok()?;
81    let trimmed = value.trim();
82    if trimmed.is_empty() {
83        None
84    } else {
85        Some(PathBuf::from(trimmed))
86    }
87}
88
89fn user_home() -> std::io::Result<PathBuf> {
90    std::env::var_os("HOME")
91        .map(PathBuf::from)
92        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "HOME is not set"))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    const TEST_RESOLVER: PathResolver = PathResolver {
100        home_env: "TEST_AGENT_HOME",
101        data_env: "TEST_AGENT_DATA",
102        project_env: "TEST_AGENT_PROJECT",
103        config_dir_name: ".test-agent",
104        data_dir_name: "test-agent",
105    };
106
107    #[test]
108    fn resolves_from_explicit_dirs() {
109        let paths = ResolvedPaths::from_dirs(PathBuf::from("/cfg"), PathBuf::from("/data"), PathBuf::from("/repo"));
110
111        assert_eq!(paths.config_dir, PathBuf::from("/cfg"));
112        assert_eq!(paths.data_dir, PathBuf::from("/data"));
113        assert_eq!(paths.project_dir, PathBuf::from("/repo"));
114    }
115
116    #[test]
117    fn resolver_exposes_static_names() {
118        assert_eq!(TEST_RESOLVER.config_dir_name, ".test-agent");
119        assert_eq!(TEST_RESOLVER.data_dir_name, "test-agent");
120    }
121}