app_path/
lib.rs

1use std::env::current_exe;
2use std::path::{Path, PathBuf};
3
4/// Creates paths relative to the executable location for applications.
5/// All files and directories stay together with the executable.
6#[derive(Clone, Debug)]
7pub struct AppPath {
8    relative_path: PathBuf,
9    full_path: PathBuf,
10}
11
12impl AppPath {
13    /// Creates file paths relative to the executable location.
14    pub fn new(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
15        let exe_dir = current_exe()?
16            .parent()
17            .ok_or_else(|| {
18                std::io::Error::new(
19                    std::io::ErrorKind::NotFound,
20                    "Could not determine executable parent directory",
21                )
22            })?
23            .to_path_buf();
24
25        Ok(Self {
26            relative_path: path.as_ref().to_path_buf(),
27            full_path: exe_dir.join(path),
28        })
29    }
30
31    /// Override the base directory (useful for testing or custom layouts)
32    pub fn with_base(mut self, base: impl AsRef<Path>) -> Self {
33        self.full_path = base.as_ref().join(&self.relative_path);
34        self
35    }
36
37    /// Get the relative portion of the path
38    pub fn relative(&self) -> &Path {
39        &self.relative_path
40    }
41
42    /// Get the full resolved path
43    pub fn full(&self) -> &Path {
44        &self.full_path
45    }
46
47    /// Check if the path exists
48    pub fn exists(&self) -> bool {
49        self.full_path.exists()
50    }
51
52    /// Create parent directories if they don't exist
53    pub fn create_dir_all(&self) -> std::io::Result<()> {
54        if let Some(parent) = self.full_path.parent() {
55            std::fs::create_dir_all(parent)?;
56        }
57        Ok(())
58    }
59}
60
61impl std::fmt::Display for AppPath {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(f, "{}", self.full_path.display())
64    }
65}
66
67impl From<AppPath> for PathBuf {
68    fn from(relative_path: AppPath) -> Self {
69        relative_path.full_path
70    }
71}
72impl AsRef<Path> for AppPath {
73    #[inline]
74    fn as_ref(&self) -> &Path {
75        self.full_path.as_ref()
76    }
77}
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use std::env;
82    use std::fs::{self, File};
83    use std::io::Write;
84    use std::path::Path;
85
86    /// Helper to create a file at a given path for testing.
87    fn create_test_file(path: &Path) {
88        if let Some(parent) = path.parent() {
89            fs::create_dir_all(parent).unwrap();
90        }
91        let mut file = File::create(path).unwrap();
92        writeln!(file, "test").unwrap();
93    }
94
95    #[test]
96    fn resolves_relative_path_to_exe_dir() {
97        // Simulate a file relative to the executable
98        let rel = "myconfig.toml";
99        let rel_path = AppPath::new(rel).unwrap();
100        let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
101        let expected = exe_dir.join(rel);
102
103        assert_eq!(rel_path.full_path, expected);
104        assert!(rel_path.full_path.is_absolute());
105    }
106
107    #[test]
108    fn resolves_relative_path_with_custom_base() {
109        // Use a temp directory as the base
110        let temp_dir = env::temp_dir().join("app_path_test_base");
111        let _ = fs::remove_dir_all(&temp_dir);
112        fs::create_dir_all(&temp_dir).unwrap();
113
114        let rel = "subdir/file.txt";
115        let rel_path = AppPath::new(rel).unwrap().with_base(&temp_dir);
116        let expected = temp_dir.join(rel);
117
118        assert_eq!(rel_path.full_path, expected);
119        assert!(rel_path.full_path.is_absolute());
120    }
121
122    #[test]
123    fn can_access_file_using_full_path() {
124        // Actually create a file and check that the full path points to it
125        let temp_dir = env::temp_dir().join("app_path_test_access");
126        let file_name = "access.txt";
127        let file_path = temp_dir.join(file_name);
128        let _ = fs::remove_dir_all(&temp_dir);
129        fs::create_dir_all(&temp_dir).unwrap();
130        create_test_file(&file_path);
131
132        let rel_path = AppPath::new(file_name).unwrap().with_base(&temp_dir);
133        assert!(rel_path.exists());
134        assert_eq!(rel_path.full_path, file_path);
135    }
136
137    #[test]
138    fn handles_dot_and_dotdot_components() {
139        let temp_dir = env::temp_dir().join("app_path_test_dot");
140        let _ = fs::remove_dir_all(&temp_dir);
141        fs::create_dir_all(&temp_dir).unwrap();
142
143        let rel = "./foo/../bar.txt";
144        let rel_path = AppPath::new(rel).unwrap().with_base(&temp_dir);
145        let expected = temp_dir.join(rel);
146
147        assert_eq!(rel_path.full_path, expected);
148    }
149
150    #[test]
151    fn as_ref_and_into_pathbuf_are_consistent() {
152        let rel = "somefile.txt";
153        let rel_path = AppPath::new(rel).unwrap();
154        let as_ref_path: &Path = rel_path.as_ref();
155        let into_pathbuf: PathBuf = rel_path.clone().into();
156        assert_eq!(as_ref_path, into_pathbuf.as_path());
157    }
158
159    #[test]
160    fn test_relative_method() {
161        let rel = "config/app.toml";
162        let rel_path = AppPath::new(rel).unwrap();
163        assert_eq!(rel_path.relative(), Path::new(rel));
164    }
165
166    #[test]
167    fn test_full_method() {
168        let rel = "data/file.txt";
169        let temp_dir = env::temp_dir().join("app_path_test_full");
170        let rel_path = AppPath::new(rel).unwrap().with_base(&temp_dir);
171        assert_eq!(rel_path.full(), temp_dir.join(rel));
172    }
173
174    #[test]
175    fn test_exists_method() {
176        let temp_dir = env::temp_dir().join("app_path_test_exists");
177        let _ = fs::remove_dir_all(&temp_dir);
178        fs::create_dir_all(&temp_dir).unwrap();
179
180        let file_name = "exists_test.txt";
181        let file_path = temp_dir.join(file_name);
182        create_test_file(&file_path);
183
184        let rel_path = AppPath::new(file_name).unwrap().with_base(&temp_dir);
185        assert!(rel_path.exists());
186
187        let non_existent = AppPath::new("non_existent.txt")
188            .unwrap()
189            .with_base(&temp_dir);
190        assert!(!non_existent.exists());
191    }
192
193    #[test]
194    fn test_create_dir_all() {
195        let temp_dir = env::temp_dir().join("app_path_test_create");
196        let _ = fs::remove_dir_all(&temp_dir);
197
198        let rel = "deep/nested/dir/file.txt";
199        let rel_path = AppPath::new(rel).unwrap().with_base(&temp_dir);
200
201        rel_path.create_dir_all().unwrap();
202        assert!(rel_path.full_path.parent().unwrap().exists());
203    }
204
205    #[test]
206    fn test_display_trait() {
207        let rel = "display_test.txt";
208        let temp_dir = env::temp_dir().join("app_path_test_display");
209        let rel_path = AppPath::new(rel).unwrap().with_base(&temp_dir);
210
211        let expected = temp_dir.join(rel);
212        assert_eq!(format!("{}", rel_path), format!("{}", expected.display()));
213    }
214}