Skip to main content

kata/
paths.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use directories::ProjectDirs;
3
4use crate::error::{Error, Result};
5
6/// PJ-side state directory name (under each managed project root).
7pub const PJ_STATE_DIR: &str = ".kata";
8/// PJ-side state file name (under `<pj>/.kata/`).
9pub const APPLIED_FILE: &str = "applied.toml";
10/// Global config file name (under `<config-dir>/kata/`).
11pub const GLOBAL_CONFIG_FILE: &str = "config.toml";
12
13/// Returns the directory used for kata's global config.
14///
15/// Resolution order:
16/// 1. `$KATA_HOME` (test-friendly override; the whole kata config tree
17///    is rooted here).
18/// 2. Platform default via `directories::ProjectDirs` (e.g.
19///    `~/.config/kata/` on Linux, `%APPDATA%\kata\config\` on Windows).
20pub fn global_config_dir() -> Result<Utf8PathBuf> {
21    if let Some(home) = std::env::var_os("KATA_HOME") {
22        let pb = Utf8PathBuf::from_path_buf(home.into())
23            .map_err(|p| Error::Config(format!("KATA_HOME is not valid UTF-8: {}", p.display())))?;
24        return Ok(pb);
25    }
26    let pd = ProjectDirs::from("", "yukimemi", "kata").ok_or_else(|| {
27        Error::Config("could not determine platform config directory".to_string())
28    })?;
29    Utf8PathBuf::from_path_buf(pd.config_dir().to_path_buf())
30        .map_err(|p| Error::Config(format!("config dir is not valid UTF-8: {}", p.display())))
31}
32
33/// Returns the path to the global `config.toml`.
34pub fn global_config_path() -> Result<Utf8PathBuf> {
35    Ok(global_config_dir()?.join(GLOBAL_CONFIG_FILE))
36}
37
38/// Returns the directory used for cached template repositories.
39///
40/// Resolution: `$KATA_HOME/cache/templates/` if set, otherwise the
41/// platform cache dir (e.g. `~/.cache/kata/templates/`).
42pub fn template_cache_dir() -> Result<Utf8PathBuf> {
43    if let Some(home) = std::env::var_os("KATA_HOME") {
44        let pb = Utf8PathBuf::from_path_buf(home.into())
45            .map_err(|p| Error::Config(format!("KATA_HOME is not valid UTF-8: {}", p.display())))?;
46        return Ok(pb.join("cache").join("templates"));
47    }
48    let pd = ProjectDirs::from("", "yukimemi", "kata")
49        .ok_or_else(|| Error::Config("could not determine platform cache directory".to_string()))?;
50    let cache = Utf8PathBuf::from_path_buf(pd.cache_dir().to_path_buf())
51        .map_err(|p| Error::Config(format!("cache dir is not valid UTF-8: {}", p.display())))?;
52    Ok(cache.join("templates"))
53}
54
55/// `<pj_root>/.kata/applied.toml`
56pub fn applied_path(pj_root: &Utf8Path) -> Utf8PathBuf {
57    pj_root.join(PJ_STATE_DIR).join(APPLIED_FILE)
58}
59
60/// Walk up from `start` looking for the nearest directory containing
61/// `.kata/applied.toml`. Returns the project root (the dir containing
62/// `.kata/`), not the state file itself.
63pub fn find_pj_root(start: &Utf8Path) -> Option<Utf8PathBuf> {
64    let mut cur: Option<&Utf8Path> = Some(start);
65    while let Some(dir) = cur {
66        if dir.join(PJ_STATE_DIR).join(APPLIED_FILE).is_file() {
67            return Some(dir.to_path_buf());
68        }
69        cur = dir.parent();
70    }
71    None
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use tempfile::TempDir;
78
79    #[test]
80    fn applied_path_joins_correctly() {
81        let p = Utf8Path::new("/tmp/myproj");
82        let expected = Utf8PathBuf::from("/tmp/myproj")
83            .join(PJ_STATE_DIR)
84            .join(APPLIED_FILE);
85        assert_eq!(applied_path(p), expected);
86    }
87
88    #[test]
89    fn find_pj_root_walks_up() {
90        let td = TempDir::new().unwrap();
91        let root = Utf8PathBuf::from_path_buf(td.path().to_path_buf()).unwrap();
92        let nested = root.join("a").join("b").join("c");
93        std::fs::create_dir_all(&nested).unwrap();
94        std::fs::create_dir_all(root.join(PJ_STATE_DIR)).unwrap();
95        std::fs::write(root.join(PJ_STATE_DIR).join(APPLIED_FILE), "").unwrap();
96
97        let found = find_pj_root(&nested).expect("should find ancestor");
98        assert_eq!(
99            std::fs::canonicalize(found.as_std_path()).unwrap(),
100            std::fs::canonicalize(root.as_std_path()).unwrap()
101        );
102    }
103
104    #[test]
105    fn find_pj_root_returns_none_when_missing() {
106        let td = TempDir::new().unwrap();
107        let root = Utf8PathBuf::from_path_buf(td.path().to_path_buf()).unwrap();
108        assert!(find_pj_root(&root).is_none());
109    }
110}