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