1use std::path::PathBuf;
2
3use crate::env::Env;
4use crate::error::{Result, YtError};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Dirs {
9 pub config: PathBuf,
10 pub cache: PathBuf,
11 pub state: PathBuf,
12}
13
14#[cfg(target_os = "macos")]
15const CACHE_FALLBACK: &str = "Library/Caches";
16#[cfg(not(target_os = "macos"))]
17const CACHE_FALLBACK: &str = ".cache";
18
19impl Dirs {
20 pub fn from_env(env: &Env) -> Result<Self> {
21 let home = env.path("HOME");
22 let base = |xdg: &str, fallback: &str| -> Result<PathBuf> {
23 env.path(xdg)
24 .or_else(|| home.as_ref().map(|home| home.join(fallback)))
25 .ok_or_else(|| YtError::NoHome.into())
26 };
27 let config = match env.path("YT_CONFIG_DIR") {
28 Some(path) => path,
29 None => base("XDG_CONFIG_HOME", ".config")?.join("yt"),
30 };
31 let cache = match env.path("YT_CACHE_DIR") {
32 Some(path) => path,
33 None => base("XDG_CACHE_HOME", CACHE_FALLBACK)?.join("yt"),
34 };
35 let state = match env.path("YT_STATE_DIR") {
36 Some(path) => path,
37 None => base("XDG_STATE_HOME", ".local/state")?.join("yt"),
38 };
39 Ok(Self {
40 config,
41 cache,
42 state,
43 })
44 }
45
46 pub fn config_file(&self) -> PathBuf {
47 self.config.join("config.toml")
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn defaults_follow_xdg_under_home() {
57 let dirs = Dirs::from_env(&Env::from_vars([("HOME", "/home/jane")])).unwrap();
58 assert_eq!(dirs.config, PathBuf::from("/home/jane/.config/yt"));
59 assert_eq!(dirs.state, PathBuf::from("/home/jane/.local/state/yt"));
60 assert_eq!(
61 dirs.config_file(),
62 PathBuf::from("/home/jane/.config/yt/config.toml")
63 );
64 #[cfg(target_os = "macos")]
65 assert_eq!(dirs.cache, PathBuf::from("/home/jane/Library/Caches/yt"));
66 #[cfg(not(target_os = "macos"))]
67 assert_eq!(dirs.cache, PathBuf::from("/home/jane/.cache/yt"));
68 }
69
70 #[test]
71 fn xdg_variables_win_over_home() {
72 let env = Env::from_vars([
73 ("HOME", "/home/jane"),
74 ("XDG_CONFIG_HOME", "/x/config"),
75 ("XDG_CACHE_HOME", "/x/cache"),
76 ("XDG_STATE_HOME", "/x/state"),
77 ]);
78 let dirs = Dirs::from_env(&env).unwrap();
79 assert_eq!(dirs.config, PathBuf::from("/x/config/yt"));
80 assert_eq!(dirs.cache, PathBuf::from("/x/cache/yt"));
81 assert_eq!(dirs.state, PathBuf::from("/x/state/yt"));
82 }
83
84 #[test]
85 fn yt_variables_win_over_everything() {
86 let env = Env::from_vars([
87 ("XDG_CONFIG_HOME", "/x/config"),
88 ("YT_CONFIG_DIR", "/y/config"),
89 ("YT_CACHE_DIR", "/y/cache"),
90 ("YT_STATE_DIR", "/y/state"),
91 ]);
92 let dirs = Dirs::from_env(&env).unwrap();
93 assert_eq!(dirs.config, PathBuf::from("/y/config"));
94 assert_eq!(dirs.cache, PathBuf::from("/y/cache"));
95 assert_eq!(dirs.state, PathBuf::from("/y/state"));
96 }
97
98 #[test]
99 fn missing_home_is_an_error() {
100 let error = Dirs::from_env(&Env::from_vars([("X", "y")])).unwrap_err();
101 assert!(matches!(
102 error.downcast_ref::<YtError>(),
103 Some(YtError::NoHome)
104 ));
105 }
106}