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