Skip to main content

wordchipper_disk_cache/
path_resolver.rs

1//! # App Path Resolver
2//!
3//! Static library defaults for cache/data directory resolution.
4
5use directories_next::ProjectDirs;
6use std::env;
7use std::path::{Path, PathBuf};
8
9/// Static configuration for application path resolution.
10pub struct PathResolver {
11    /// The qualifier for [`ProjectDirs`].
12    pub qualifier: &'static str,
13
14    /// The organization for [`ProjectDirs`].
15    pub organization: &'static str,
16
17    /// The application for [`ProjectDirs`].
18    pub application: &'static str,
19
20    /// The resolution order for cache directories environment variables.
21    pub cache_env_vars: &'static [&'static str],
22
23    /// The resolution order for data directories environment variables.
24    pub data_env_vars: &'static [&'static str],
25}
26
27impl PathResolver {
28    /// Get the [`ProjectDirs`] for this config.
29    pub fn project_dirs(&self) -> Option<ProjectDirs> {
30        ProjectDirs::from(self.organization, self.application, self.qualifier)
31    }
32
33    /// Resolve the cache directory for this config.
34    ///
35    /// Resolution Order:
36    /// 1. `path`, if present.
37    /// 2. ``env[$VAR]`` for each `self.cache_env_vars`; in order.
38    /// 3. `self.project_dirs().cache_dir()`, if present.
39    /// 4. `None`
40    ///
41    /// ## Project Dirs Behavior
42    ///
43    /// |Platform | Value                                                                 | Example                                             |
44    /// | ------- | --------------------------------------------------------------------- | --------------------------------------------------- |
45    /// | Linux   | `$XDG_CACHE_HOME`/`_project_path_` or `$HOME`/.cache/`_project_path_` | /home/alice/.cache/barapp                           |
46    /// | macOS   | `$HOME`/Library/Caches/`_project_path_`                               | /Users/Alice/Library/Caches/com.Foo-Corp.Bar-App    |
47    /// | Windows | `{FOLDERID_LocalAppData}`\\`_project_path_`\\cache                    | C:\Users\Alice\AppData\Local\Foo Corp\Bar App\cache |
48    pub fn resolve_cache_dir<P: AsRef<Path>>(
49        &self,
50        path: Option<P>,
51    ) -> Option<PathBuf> {
52        if let Some(path) = path.as_ref() {
53            return Some(path.as_ref().to_path_buf());
54        }
55
56        for env_var in self.cache_env_vars {
57            if let Ok(path) = env::var(env_var) {
58                return Some(PathBuf::from(path));
59            }
60        }
61
62        if let Some(pds) = self.project_dirs() {
63            return Some(pds.cache_dir().to_path_buf());
64        }
65
66        None
67    }
68
69    /// Resolve the data directory for this config.
70    ///
71    /// Resolution Order:
72    /// 1. `path`, if present.
73    /// 2. ``env[$VAR]`` for each `self.data_env_vars`; in order.
74    /// 3. `self.project_dirs().data_dirs()`, if present.
75    /// 4. `None`
76    ///
77    /// ## Project Dirs Behavior
78    ///
79    /// |Platform | Value                                                                      | Example                                                       |
80    /// | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------- |
81    /// | Linux   | `$XDG_DATA_HOME`/`_project_path_` or `$HOME`/.local/share/`_project_path_` | /home/alice/.local/share/barapp                               |
82    /// | macOS   | `$HOME`/Library/Application Support/`_project_path_`                       | /Users/Alice/Library/Application Support/com.Foo-Corp.Bar-App |
83    /// | Windows | `{FOLDERID_LocalAppData}`\\`_project_path_`\\data                          | C:\Users\Alice\AppData\Local\Foo Corp\Bar App\data            |
84    pub fn resolve_data_dir<P: AsRef<Path>>(
85        &self,
86        path: Option<P>,
87    ) -> Option<PathBuf> {
88        if let Some(path) = path.as_ref() {
89            return Some(path.as_ref().to_path_buf());
90        }
91
92        for env_var in self.data_env_vars {
93            if let Ok(path) = env::var(env_var) {
94                return Some(PathBuf::from(path));
95            }
96        }
97
98        if let Some(pds) = self.project_dirs() {
99            return Some(pds.data_dir().to_path_buf());
100        }
101
102        None
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use serial_test::serial;
110
111    const CACHE_ENV1: &str = "_APP_PATH_CACHE_ENV1";
112    const CACHE_ENV2: &str = "_APP_PATH_CACHE_ENV2";
113    const DATA_ENV1: &str = "_APP_PATH_DATA_ENV1";
114    const DATA_ENV2: &str = "_APP_PATH_DATA_ENV2";
115
116    const TEST_CONFIG: PathResolver = PathResolver {
117        qualifier: "io",
118        organization: "crates",
119        application: "example",
120        cache_env_vars: &[CACHE_ENV1, CACHE_ENV2],
121        data_env_vars: &[DATA_ENV1, DATA_ENV2],
122    };
123
124    #[test]
125    #[serial]
126    fn test_resolve_dirs() {
127        let pds = TEST_CONFIG
128            .project_dirs()
129            .expect("failed to get project dirs");
130
131        let no_path: Option<PathBuf> = None;
132
133        let user_cache_dir = PathBuf::from("/tmp/app_cache/cache");
134        let user_data_dir = PathBuf::from("/tmp/app_cache/data");
135
136        let env_cache_dir1 = PathBuf::from("/tmp/app_cache/env_cache.1");
137        let env_cache_dir2 = PathBuf::from("/tmp/app_cache/env_cache.2");
138        let env_data_dir1 = PathBuf::from("/tmp/app_cache/env_data.1");
139        let env_data_dir2 = PathBuf::from("/tmp/app_cache/env_data.2");
140
141        // No env vars
142        unsafe {
143            for v in TEST_CONFIG.cache_env_vars {
144                env::remove_var(v);
145            }
146            for v in TEST_CONFIG.data_env_vars {
147                env::remove_var(v);
148            }
149        }
150
151        // User overrides.
152        assert_eq!(
153            TEST_CONFIG.resolve_cache_dir(Some(user_cache_dir.clone())),
154            Some(user_cache_dir.clone()),
155        );
156        assert_eq!(
157            TEST_CONFIG.resolve_data_dir(Some(user_data_dir.clone())),
158            Some(user_data_dir.clone()),
159        );
160
161        // Resolution; use project dirs.
162        assert_eq!(
163            TEST_CONFIG.resolve_cache_dir(no_path.clone()),
164            Some(pds.cache_dir().to_path_buf())
165        );
166        assert_eq!(
167            TEST_CONFIG.resolve_data_dir(no_path.clone()),
168            Some(pds.data_dir().to_path_buf())
169        );
170
171        // Lowest priority dirs.
172        unsafe {
173            env::set_var(CACHE_ENV2, env_cache_dir2.to_str().unwrap());
174            env::set_var(DATA_ENV2, env_data_dir2.to_str().unwrap());
175        }
176
177        // User overrides.
178        assert_eq!(
179            TEST_CONFIG.resolve_cache_dir(Some(user_cache_dir.clone())),
180            Some(user_cache_dir.clone()),
181        );
182        assert_eq!(
183            TEST_CONFIG.resolve_data_dir(Some(user_data_dir.clone())),
184            Some(user_data_dir.clone()),
185        );
186
187        // Resolution; use env vars.
188        assert_eq!(
189            TEST_CONFIG.resolve_cache_dir(no_path.clone()),
190            Some(env_cache_dir2.clone())
191        );
192        assert_eq!(
193            TEST_CONFIG.resolve_data_dir(no_path.clone()),
194            Some(env_data_dir2.clone())
195        );
196
197        // Higher priority dirs.
198        unsafe {
199            env::set_var(CACHE_ENV1, env_cache_dir1.to_str().unwrap());
200            env::set_var(DATA_ENV1, env_data_dir1.to_str().unwrap());
201        }
202
203        // User overrides.
204        assert_eq!(
205            TEST_CONFIG.resolve_cache_dir(Some(user_cache_dir.clone())),
206            Some(user_cache_dir.clone()),
207        );
208        assert_eq!(
209            TEST_CONFIG.resolve_data_dir(Some(user_data_dir.clone())),
210            Some(user_data_dir.clone()),
211        );
212
213        // Resolution; use env vars.
214        assert_eq!(
215            TEST_CONFIG.resolve_cache_dir(no_path.clone()),
216            Some(env_cache_dir1.clone())
217        );
218        assert_eq!(
219            TEST_CONFIG.resolve_data_dir(no_path.clone()),
220            Some(env_data_dir1.clone())
221        );
222    }
223}