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