project_rag/
paths.rs

1/// Centralized platform-specific path computation
2///
3/// Provides consistent path handling across Windows, macOS, and Linux following
4/// XDG Base Directory specification on Unix-like systems.
5use std::path::PathBuf;
6
7/// The folder name used for data storage.
8/// Default: "project-rag"
9/// With `alt-folder-name` feature: Uses alternative folder name "brainwires" instead of "project-rag".
10#[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
16/// Platform-agnostic path utilities
17pub struct PlatformPaths;
18
19impl PlatformPaths {
20    /// Get the appropriate data directory for the current platform
21    ///
22    /// - Windows: %LOCALAPPDATA%
23    /// - macOS: ~/Library/Application Support
24    /// - Linux/Unix: $XDG_DATA_HOME or ~/.local/share
25    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            // Linux/Unix - follow XDG Base Directory specification
36            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    /// Get the appropriate cache directory for the current platform
46    ///
47    /// - Windows: %LOCALAPPDATA%
48    /// - macOS: ~/Library/Caches
49    /// - Linux/Unix: $XDG_CACHE_HOME or ~/.cache
50    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            // Linux/Unix - follow XDG Base Directory specification
61            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    /// Get the appropriate config directory for the current platform
69    ///
70    /// - Windows: %APPDATA%
71    /// - macOS: ~/Library/Application Support
72    /// - Linux/Unix: $XDG_CONFIG_HOME or ~/.config
73    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            // Linux/Unix - follow XDG Base Directory specification
84            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    /// Get the project folder name
92    ///
93    /// Returns: "project-rag"
94    pub fn project_folder_name() -> &'static str {
95        PROJECT_FOLDER_NAME
96    }
97
98    /// Get default project-specific data directory
99    ///
100    /// Returns: {data_dir}/{project_folder_name}
101    pub fn project_data_dir() -> PathBuf {
102        Self::data_dir().join(PROJECT_FOLDER_NAME)
103    }
104
105    /// Get default project-specific cache directory
106    ///
107    /// Returns: {cache_dir}/{project_folder_name}
108    pub fn project_cache_dir() -> PathBuf {
109        Self::cache_dir().join(PROJECT_FOLDER_NAME)
110    }
111
112    /// Get default project-specific config directory
113    ///
114    /// Returns: {config_dir}/{project_folder_name}
115    pub fn project_config_dir() -> PathBuf {
116        Self::config_dir().join(PROJECT_FOLDER_NAME)
117    }
118
119    /// Get default LanceDB database path
120    ///
121    /// Returns: {data_dir}/{project_folder_name}/lancedb
122    pub fn default_lancedb_path() -> PathBuf {
123        Self::project_data_dir().join("lancedb")
124    }
125
126    /// Get default hash cache path
127    ///
128    /// Returns: {cache_dir}/{project_folder_name}/hash_cache.json
129    pub fn default_hash_cache_path() -> PathBuf {
130        Self::project_cache_dir().join("hash_cache.json")
131    }
132
133    /// Get default git cache path
134    ///
135    /// Returns: {cache_dir}/{project_folder_name}/git_cache.json
136    pub fn default_git_cache_path() -> PathBuf {
137        Self::project_cache_dir().join("git_cache.json")
138    }
139
140    /// Get default config file path
141    ///
142    /// Returns: {config_dir}/{project_folder_name}/config.toml
143    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        // Paths should either be absolute or fallback to "."
213        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        // Test that XDG_DATA_HOME is respected
221        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        // Restore original value
230        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        // Test fallback to HOME/.local/share when XDG_DATA_HOME is not set
242        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        // Restore original values
254        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        // Test that XDG_CACHE_HOME is respected
270        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        // Restore original value
279        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        // Test fallback to HOME/.cache when XDG_CACHE_HOME is not set
291        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        // Restore original values
303        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        // Test that XDG_CONFIG_HOME is respected
319        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        // Restore original value
328        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        // Test fallback to HOME/.config when XDG_CONFIG_HOME is not set
340        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        // Restore original values
352        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        // Verify that project-specific dirs are subdirectories of base dirs
367        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        // Test that specific file paths include expected components
387        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        // All should contain project name
393        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        // Specific components
407        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}