1use std::env::current_exe;
2use std::path::{Path, PathBuf};
3
4#[derive(Clone, Debug)]
7pub struct AppPath {
8 relative_path: PathBuf,
9 full_path: PathBuf,
10}
11
12impl AppPath {
13 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 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 pub fn relative(&self) -> &Path {
39 &self.relative_path
40 }
41
42 pub fn full(&self) -> &Path {
44 &self.full_path
45 }
46
47 pub fn exists(&self) -> bool {
49 self.full_path.exists()
50 }
51
52 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 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 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 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 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}