1use std::path::PathBuf;
6
7#[cfg(not(feature = "alt-folder-name"))]
11const PROJECT_FOLDER_NAME: &str = "project-rag";
12
13#[cfg(feature = "alt-folder-name")]
14const PROJECT_FOLDER_NAME: &str = "brainwires";
15
16pub struct PlatformPaths;
18
19impl PlatformPaths {
20 pub fn data_dir() -> PathBuf {
26 if cfg!(target_os = "windows") {
27 std::env::var("LOCALAPPDATA")
28 .map(PathBuf::from)
29 .unwrap_or_else(|_| PathBuf::from("."))
30 } else if cfg!(target_os = "macos") {
31 std::env::var("HOME")
32 .map(|home| PathBuf::from(home).join("Library/Application Support"))
33 .unwrap_or_else(|_| PathBuf::from("."))
34 } else {
35 std::env::var("XDG_DATA_HOME")
37 .map(PathBuf::from)
38 .or_else(|_| {
39 std::env::var("HOME").map(|home| PathBuf::from(home).join(".local/share"))
40 })
41 .unwrap_or_else(|_| PathBuf::from("."))
42 }
43 }
44
45 pub fn cache_dir() -> PathBuf {
51 if cfg!(target_os = "windows") {
52 std::env::var("LOCALAPPDATA")
53 .map(PathBuf::from)
54 .unwrap_or_else(|_| PathBuf::from("."))
55 } else if cfg!(target_os = "macos") {
56 std::env::var("HOME")
57 .map(|home| PathBuf::from(home).join("Library/Caches"))
58 .unwrap_or_else(|_| PathBuf::from("."))
59 } else {
60 std::env::var("XDG_CACHE_HOME")
62 .map(PathBuf::from)
63 .or_else(|_| std::env::var("HOME").map(|home| PathBuf::from(home).join(".cache")))
64 .unwrap_or_else(|_| PathBuf::from("."))
65 }
66 }
67
68 pub fn config_dir() -> PathBuf {
74 if cfg!(target_os = "windows") {
75 std::env::var("APPDATA")
76 .map(PathBuf::from)
77 .unwrap_or_else(|_| PathBuf::from("."))
78 } else if cfg!(target_os = "macos") {
79 std::env::var("HOME")
80 .map(|home| PathBuf::from(home).join("Library/Application Support"))
81 .unwrap_or_else(|_| PathBuf::from("."))
82 } else {
83 std::env::var("XDG_CONFIG_HOME")
85 .map(PathBuf::from)
86 .or_else(|_| std::env::var("HOME").map(|home| PathBuf::from(home).join(".config")))
87 .unwrap_or_else(|_| PathBuf::from("."))
88 }
89 }
90
91 pub fn project_folder_name() -> &'static str {
95 PROJECT_FOLDER_NAME
96 }
97
98 pub fn project_data_dir() -> PathBuf {
102 Self::data_dir().join(PROJECT_FOLDER_NAME)
103 }
104
105 pub fn project_cache_dir() -> PathBuf {
109 Self::cache_dir().join(PROJECT_FOLDER_NAME)
110 }
111
112 pub fn project_config_dir() -> PathBuf {
116 Self::config_dir().join(PROJECT_FOLDER_NAME)
117 }
118
119 pub fn default_lancedb_path() -> PathBuf {
123 Self::project_data_dir().join("lancedb")
124 }
125
126 pub fn default_hash_cache_path() -> PathBuf {
130 Self::project_cache_dir().join("hash_cache.json")
131 }
132
133 pub fn default_git_cache_path() -> PathBuf {
137 Self::project_cache_dir().join("git_cache.json")
138 }
139
140 pub fn default_config_path() -> PathBuf {
144 Self::project_config_dir().join("config.toml")
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::env;
152
153 #[test]
154 fn test_data_dir_not_empty() {
155 let dir = PlatformPaths::data_dir();
156 assert!(!dir.as_os_str().is_empty());
157 }
158
159 #[test]
160 fn test_cache_dir_not_empty() {
161 let dir = PlatformPaths::cache_dir();
162 assert!(!dir.as_os_str().is_empty());
163 }
164
165 #[test]
166 fn test_config_dir_not_empty() {
167 let dir = PlatformPaths::config_dir();
168 assert!(!dir.as_os_str().is_empty());
169 }
170
171 #[test]
172 fn test_project_paths_contain_project_name() {
173 let data_dir = PlatformPaths::project_data_dir();
174 let cache_dir = PlatformPaths::project_cache_dir();
175 let config_dir = PlatformPaths::project_config_dir();
176
177 assert!(data_dir.to_string_lossy().contains("project-rag"));
178 assert!(cache_dir.to_string_lossy().contains("project-rag"));
179 assert!(config_dir.to_string_lossy().contains("project-rag"));
180 }
181
182 #[test]
183 fn test_default_lancedb_path() {
184 let path = PlatformPaths::default_lancedb_path();
185 assert!(path.to_string_lossy().contains("project-rag"));
186 assert!(path.to_string_lossy().contains("lancedb"));
187 }
188
189 #[test]
190 fn test_default_hash_cache_path() {
191 let path = PlatformPaths::default_hash_cache_path();
192 assert!(path.to_string_lossy().contains("project-rag"));
193 assert!(path.to_string_lossy().contains("hash_cache.json"));
194 }
195
196 #[test]
197 fn test_default_git_cache_path() {
198 let path = PlatformPaths::default_git_cache_path();
199 assert!(path.to_string_lossy().contains("project-rag"));
200 assert!(path.to_string_lossy().contains("git_cache.json"));
201 }
202
203 #[test]
204 fn test_default_config_path() {
205 let path = PlatformPaths::default_config_path();
206 assert!(path.to_string_lossy().contains("project-rag"));
207 assert!(path.to_string_lossy().contains("config.toml"));
208 }
209
210 #[test]
211 fn test_paths_are_absolute_or_relative() {
212 let data_dir = PlatformPaths::data_dir();
214 assert!(data_dir.is_absolute() || data_dir == PathBuf::from("."));
215 }
216
217 #[test]
218 #[cfg(target_os = "linux")]
219 fn test_data_dir_with_xdg_data_home() {
220 let original = env::var("XDG_DATA_HOME").ok();
222 unsafe {
223 env::set_var("XDG_DATA_HOME", "/custom/data");
224 }
225
226 let dir = PlatformPaths::data_dir();
227 assert_eq!(dir, PathBuf::from("/custom/data"));
228
229 unsafe {
231 match original {
232 Some(val) => env::set_var("XDG_DATA_HOME", val),
233 None => env::remove_var("XDG_DATA_HOME"),
234 }
235 }
236 }
237
238 #[test]
239 #[cfg(target_os = "linux")]
240 fn test_data_dir_fallback_to_home() {
241 let xdg_original = env::var("XDG_DATA_HOME").ok();
243 let home_original = env::var("HOME").ok();
244
245 unsafe {
246 env::remove_var("XDG_DATA_HOME");
247 env::set_var("HOME", "/home/testuser");
248 }
249
250 let dir = PlatformPaths::data_dir();
251 assert_eq!(dir, PathBuf::from("/home/testuser/.local/share"));
252
253 unsafe {
255 match xdg_original {
256 Some(val) => env::set_var("XDG_DATA_HOME", val),
257 None => env::remove_var("XDG_DATA_HOME"),
258 }
259 match home_original {
260 Some(val) => env::set_var("HOME", val),
261 None => env::remove_var("HOME"),
262 }
263 }
264 }
265
266 #[test]
267 #[cfg(target_os = "linux")]
268 fn test_cache_dir_with_xdg_cache_home() {
269 let original = env::var("XDG_CACHE_HOME").ok();
271 unsafe {
272 env::set_var("XDG_CACHE_HOME", "/custom/cache");
273 }
274
275 let dir = PlatformPaths::cache_dir();
276 assert_eq!(dir, PathBuf::from("/custom/cache"));
277
278 unsafe {
280 match original {
281 Some(val) => env::set_var("XDG_CACHE_HOME", val),
282 None => env::remove_var("XDG_CACHE_HOME"),
283 }
284 }
285 }
286
287 #[test]
288 #[cfg(target_os = "linux")]
289 fn test_cache_dir_fallback_to_home() {
290 let xdg_original = env::var("XDG_CACHE_HOME").ok();
292 let home_original = env::var("HOME").ok();
293
294 unsafe {
295 env::remove_var("XDG_CACHE_HOME");
296 env::set_var("HOME", "/home/testuser");
297 }
298
299 let dir = PlatformPaths::cache_dir();
300 assert_eq!(dir, PathBuf::from("/home/testuser/.cache"));
301
302 unsafe {
304 match xdg_original {
305 Some(val) => env::set_var("XDG_CACHE_HOME", val),
306 None => env::remove_var("XDG_CACHE_HOME"),
307 }
308 match home_original {
309 Some(val) => env::set_var("HOME", val),
310 None => env::remove_var("HOME"),
311 }
312 }
313 }
314
315 #[test]
316 #[cfg(target_os = "linux")]
317 fn test_config_dir_with_xdg_config_home() {
318 let original = env::var("XDG_CONFIG_HOME").ok();
320 unsafe {
321 env::set_var("XDG_CONFIG_HOME", "/custom/config");
322 }
323
324 let dir = PlatformPaths::config_dir();
325 assert_eq!(dir, PathBuf::from("/custom/config"));
326
327 unsafe {
329 match original {
330 Some(val) => env::set_var("XDG_CONFIG_HOME", val),
331 None => env::remove_var("XDG_CONFIG_HOME"),
332 }
333 }
334 }
335
336 #[test]
337 #[cfg(target_os = "linux")]
338 fn test_config_dir_fallback_to_home() {
339 let xdg_original = env::var("XDG_CONFIG_HOME").ok();
341 let home_original = env::var("HOME").ok();
342
343 unsafe {
344 env::remove_var("XDG_CONFIG_HOME");
345 env::set_var("HOME", "/home/testuser");
346 }
347
348 let dir = PlatformPaths::config_dir();
349 assert_eq!(dir, PathBuf::from("/home/testuser/.config"));
350
351 unsafe {
353 match xdg_original {
354 Some(val) => env::set_var("XDG_CONFIG_HOME", val),
355 None => env::remove_var("XDG_CONFIG_HOME"),
356 }
357 match home_original {
358 Some(val) => env::set_var("HOME", val),
359 None => env::remove_var("HOME"),
360 }
361 }
362 }
363
364 #[test]
365 fn test_all_project_dirs_are_subdirectories() {
366 let data_dir = PlatformPaths::data_dir();
368 let project_data = PlatformPaths::project_data_dir();
369
370 assert!(
371 project_data.starts_with(&data_dir) || data_dir == PathBuf::from("."),
372 "project_data_dir should be subdirectory of data_dir"
373 );
374
375 let cache_dir = PlatformPaths::cache_dir();
376 let project_cache = PlatformPaths::project_cache_dir();
377
378 assert!(
379 project_cache.starts_with(&cache_dir) || cache_dir == PathBuf::from("."),
380 "project_cache_dir should be subdirectory of cache_dir"
381 );
382 }
383
384 #[test]
385 fn test_specific_file_paths() {
386 let lancedb_path = PlatformPaths::default_lancedb_path();
388 let hash_cache_path = PlatformPaths::default_hash_cache_path();
389 let git_cache_path = PlatformPaths::default_git_cache_path();
390 let config_path = PlatformPaths::default_config_path();
391
392 for path in [
394 &lancedb_path,
395 &hash_cache_path,
396 &git_cache_path,
397 &config_path,
398 ] {
399 assert!(
400 path.to_string_lossy().contains("project-rag"),
401 "Path {:?} should contain 'project-rag'",
402 path
403 );
404 }
405
406 assert!(lancedb_path.ends_with("lancedb"));
408 assert!(hash_cache_path.ends_with("hash_cache.json"));
409 assert!(git_cache_path.ends_with("git_cache.json"));
410 assert!(config_path.ends_with("config.toml"));
411 }
412}