app_path/lib.rs
1//! # app-path
2//!
3//! Create file paths relative to your executable for truly portable applications.
4//!
5//! ## Quick Start
6//!
7//! ```rust
8//! use app_path::AppPath;
9//! use std::convert::TryFrom;
10//!
11//! // Create paths relative to your executable
12//! let config = AppPath::try_new("config.toml")?;
13//! let data = AppPath::try_new("data/users.db")?;
14//!
15//! // Alternative: Use TryFrom for ergonomic conversions
16//! let logs = AppPath::try_from("logs/app.log")?;
17//! let cache_file = "cache.json".to_string();
18//! let cache = AppPath::try_from(cache_file)?;
19//!
20//! // Get the paths for use with standard library functions
21//! println!("Config: {}", config.path().display());
22//! println!("Data: {}", data.path().display());
23//!
24//! // Check existence and create directories
25//! if !logs.exists() {
26//! logs.create_dir_all()?;
27//! }
28//!
29//! # Ok::<(), Box<dyn std::error::Error>>(())
30//! ```
31
32use std::env::current_exe;
33use std::path::{Path, PathBuf};
34
35/// Creates paths relative to the executable location for applications.
36///
37/// All files and directories stay together with the executable, making
38/// your application truly portable. Perfect for:
39///
40/// - Portable applications that run from USB drives
41/// - Development tools that should work anywhere
42/// - Corporate environments where you can't install software
43///
44/// # Examples
45///
46/// ```rust
47/// use app_path::AppPath;
48///
49/// // Basic usage
50/// let config = AppPath::try_new("settings.toml")?;
51/// let data_dir = AppPath::try_new("data")?;
52///
53/// // Check if files exist
54/// if config.exists() {
55/// let settings = std::fs::read_to_string(config.path())?;
56/// }
57///
58/// // Create directories
59/// data_dir.create_dir_all()?;
60///
61/// # Ok::<(), Box<dyn std::error::Error>>(())
62/// ```
63#[derive(Clone, Debug)]
64pub struct AppPath {
65 input_path: PathBuf,
66 full_path: PathBuf,
67}
68
69impl AppPath {
70 /// Creates file paths relative to the executable location.
71 ///
72 /// # Arguments
73 ///
74 /// * `path` - A relative path that will be resolved relative to the executable
75 ///
76 /// # Returns
77 ///
78 /// Returns `Ok(AppPath)` on success, or an `std::io::Error` if the executable
79 /// directory cannot be determined.
80 ///
81 /// # Examples
82 ///
83 /// ```rust
84 /// use app_path::AppPath;
85 ///
86 /// let config = AppPath::try_new("config.toml")?;
87 /// let nested = AppPath::try_new("data/users.db")?;
88 ///
89 /// # Ok::<(), Box<dyn std::error::Error>>(())
90 /// ```
91 pub fn try_new(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
92 let exe_dir = current_exe()?
93 .parent()
94 .ok_or_else(|| {
95 std::io::Error::new(
96 std::io::ErrorKind::NotFound,
97 "Could not determine executable parent directory",
98 )
99 })?
100 .to_path_buf();
101
102 Ok(Self {
103 input_path: path.as_ref().to_path_buf(),
104 full_path: exe_dir.join(path),
105 })
106 }
107
108 /// Override the base directory (useful for testing or custom layouts).
109 ///
110 /// This method allows you to specify a different base directory instead
111 /// of using the executable's directory. Useful for testing or when you
112 /// want a different layout.
113 ///
114 /// # Arguments
115 ///
116 /// * `base` - The new base directory to use
117 ///
118 /// # Examples
119 ///
120 /// ```rust
121 /// use app_path::AppPath;
122 /// use std::env;
123 ///
124 /// let config = AppPath::try_new("config.toml")?
125 /// .with_base(env::temp_dir());
126 ///
127 /// # Ok::<(), Box<dyn std::error::Error>>(())
128 /// ```
129 pub fn with_base(mut self, base: impl AsRef<Path>) -> Self {
130 self.full_path = base.as_ref().join(&self.input_path);
131 self
132 }
133
134 /// Get the original input path (before resolution).
135 ///
136 /// Returns the path as it was originally provided to [`AppPath::try_new`],
137 /// before any resolution or joining with the base directory.
138 ///
139 /// # Examples
140 ///
141 /// ```rust
142 /// use app_path::AppPath;
143 ///
144 /// let app_path = AppPath::try_new("config/settings.toml")?;
145 /// assert_eq!(app_path.input().to_str(), Some("config/settings.toml"));
146 ///
147 /// # Ok::<(), Box<dyn std::error::Error>>(())
148 /// ```
149 #[inline]
150 pub fn input(&self) -> &Path {
151 &self.input_path
152 }
153
154 /// Get the full resolved path.
155 ///
156 /// This is the primary method for getting the actual filesystem path
157 /// where your file or directory is located.
158 ///
159 /// # Examples
160 ///
161 /// ```rust
162 /// use app_path::AppPath;
163 ///
164 /// let config = AppPath::try_new("config.toml")?;
165 ///
166 /// // Get the path for use with standard library functions
167 /// println!("Config path: {}", config.path().display());
168 ///
169 /// // The path is always absolute
170 /// assert!(config.path().is_absolute());
171 ///
172 /// # Ok::<(), Box<dyn std::error::Error>>(())
173 /// ```
174 #[inline]
175 pub fn path(&self) -> &Path {
176 &self.full_path
177 }
178
179 /// Check if the path exists.
180 ///
181 /// # Examples
182 ///
183 /// ```rust
184 /// use app_path::AppPath;
185 ///
186 /// let config = AppPath::try_new("config.toml")?;
187 ///
188 /// if config.exists() {
189 /// println!("Config file found!");
190 /// } else {
191 /// println!("Config file not found, using defaults.");
192 /// }
193 ///
194 /// # Ok::<(), Box<dyn std::error::Error>>(())
195 /// ```
196 #[inline]
197 pub fn exists(&self) -> bool {
198 self.full_path.exists()
199 }
200
201 /// Create parent directories if they don't exist.
202 ///
203 /// This is equivalent to calling [`std::fs::create_dir_all`] on the
204 /// parent directory of this path.
205 ///
206 /// # Examples
207 ///
208 /// ```rust
209 /// use app_path::AppPath;
210 /// use std::env;
211 ///
212 /// // Use a temporary directory for the example
213 /// let temp_dir = env::temp_dir().join("app_path_example");
214 /// let data_file = AppPath::try_new("data/users/profile.json")?
215 /// .with_base(&temp_dir);
216 ///
217 /// // Ensure the "data/users" directory exists
218 /// data_file.create_dir_all()?;
219 ///
220 /// // Verify the directory was created
221 /// assert!(data_file.path().parent().unwrap().exists());
222 ///
223 /// # std::fs::remove_dir_all(&temp_dir).ok();
224 /// # Ok::<(), Box<dyn std::error::Error>>(())
225 /// ```
226 pub fn create_dir_all(&self) -> std::io::Result<()> {
227 if let Some(parent) = self.full_path.parent() {
228 std::fs::create_dir_all(parent)?;
229 }
230 Ok(())
231 }
232}
233
234impl std::fmt::Display for AppPath {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 write!(f, "{}", self.full_path.display())
237 }
238}
239
240impl From<AppPath> for PathBuf {
241 #[inline]
242 fn from(app_path: AppPath) -> Self {
243 app_path.full_path
244 }
245}
246impl AsRef<Path> for AppPath {
247 #[inline]
248 fn as_ref(&self) -> &Path {
249 self.full_path.as_ref()
250 }
251}
252
253/// Ergonomic fallible conversion from string types.
254///
255/// These implementations allow you to create `AppPath` instances directly
256/// from strings, with proper error handling for the fallible operation.
257///
258/// # Examples
259///
260/// ```rust
261/// use app_path::AppPath;
262/// use std::convert::TryFrom;
263///
264/// // From &str
265/// let config = AppPath::try_from("config.toml")?;
266///
267/// // From String
268/// let data_file = "data/users.db".to_string();
269/// let data = AppPath::try_from(data_file)?;
270///
271/// // From &String
272/// let path_string = "logs/app.log".to_string();
273/// let logs = AppPath::try_from(&path_string)?;
274///
275/// # Ok::<(), Box<dyn std::error::Error>>(())
276/// ```
277impl TryFrom<&str> for AppPath {
278 type Error = std::io::Error;
279
280 fn try_from(path: &str) -> Result<Self, Self::Error> {
281 AppPath::try_new(path)
282 }
283}
284
285impl TryFrom<String> for AppPath {
286 type Error = std::io::Error;
287
288 fn try_from(path: String) -> Result<Self, Self::Error> {
289 AppPath::try_new(path)
290 }
291}
292
293impl TryFrom<&String> for AppPath {
294 type Error = std::io::Error;
295
296 fn try_from(path: &String) -> Result<Self, Self::Error> {
297 AppPath::try_new(path)
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::env;
305 use std::fs::{self, File};
306 use std::io::Write;
307 use std::path::Path;
308
309 /// Helper to create a file at a given path for testing.
310 fn create_test_file(path: &Path) {
311 if let Some(parent) = path.parent() {
312 fs::create_dir_all(parent).unwrap();
313 }
314 let mut file = File::create(path).unwrap();
315 writeln!(file, "test").unwrap();
316 }
317
318 #[test]
319 fn resolves_relative_path_to_exe_dir() {
320 // Simulate a file relative to the executable
321 let rel = "myconfig.toml";
322 let rel_path = AppPath::try_new(rel).unwrap();
323 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
324 let expected = exe_dir.join(rel);
325
326 assert_eq!(rel_path.path(), &expected);
327 assert!(rel_path.path().is_absolute());
328 }
329
330 #[test]
331 fn resolves_relative_path_with_custom_base() {
332 // Use a temp directory as the base
333 let temp_dir = env::temp_dir().join("app_path_test_base");
334 let _ = fs::remove_dir_all(&temp_dir);
335 fs::create_dir_all(&temp_dir).unwrap();
336
337 let rel = "subdir/file.txt";
338 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
339 let expected = temp_dir.join(rel);
340
341 assert_eq!(rel_path.path(), &expected);
342 assert!(rel_path.path().is_absolute());
343 }
344
345 #[test]
346 fn can_access_file_using_full_path() {
347 // Actually create a file and check that the full path points to it
348 let temp_dir = env::temp_dir().join("app_path_test_access");
349 let file_name = "access.txt";
350 let file_path = temp_dir.join(file_name);
351 let _ = fs::remove_dir_all(&temp_dir);
352 fs::create_dir_all(&temp_dir).unwrap();
353 create_test_file(&file_path);
354
355 let rel_path = AppPath::try_new(file_name).unwrap().with_base(&temp_dir);
356 assert!(rel_path.exists());
357 assert_eq!(rel_path.path(), &file_path);
358 }
359
360 #[test]
361 fn handles_dot_and_dotdot_components() {
362 let temp_dir = env::temp_dir().join("app_path_test_dot");
363 let _ = fs::remove_dir_all(&temp_dir);
364 fs::create_dir_all(&temp_dir).unwrap();
365
366 let rel = "./foo/../bar.txt";
367 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
368 let expected = temp_dir.join(rel);
369
370 assert_eq!(rel_path.path(), &expected);
371 }
372
373 #[test]
374 fn as_ref_and_into_pathbuf_are_consistent() {
375 let rel = "somefile.txt";
376 let rel_path = AppPath::try_new(rel).unwrap();
377 let as_ref_path: &Path = rel_path.as_ref();
378 let into_pathbuf: PathBuf = rel_path.clone().into();
379 assert_eq!(as_ref_path, into_pathbuf.as_path());
380 }
381
382 #[test]
383 fn test_input_method() {
384 let rel = "config/app.toml";
385 let rel_path = AppPath::try_new(rel).unwrap();
386 assert_eq!(rel_path.input(), Path::new(rel));
387 }
388
389 #[test]
390 fn test_path_method() {
391 let rel = "data/file.txt";
392 let temp_dir = env::temp_dir().join("app_path_test_full");
393 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
394 assert_eq!(rel_path.path(), temp_dir.join(rel));
395 }
396
397 #[test]
398 fn test_exists_method() {
399 let temp_dir = env::temp_dir().join("app_path_test_exists");
400 let _ = fs::remove_dir_all(&temp_dir);
401 fs::create_dir_all(&temp_dir).unwrap();
402
403 let file_name = "exists_test.txt";
404 let file_path = temp_dir.join(file_name);
405 create_test_file(&file_path);
406
407 let rel_path = AppPath::try_new(file_name).unwrap().with_base(&temp_dir);
408 assert!(rel_path.exists());
409
410 let non_existent = AppPath::try_new("non_existent.txt")
411 .unwrap()
412 .with_base(&temp_dir);
413 assert!(!non_existent.exists());
414 }
415
416 #[test]
417 fn test_create_dir_all() {
418 let temp_dir = env::temp_dir().join("app_path_test_create");
419 let _ = fs::remove_dir_all(&temp_dir);
420
421 let rel = "deep/nested/dir/file.txt";
422 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
423
424 rel_path.create_dir_all().unwrap();
425 assert!(rel_path.path().parent().unwrap().exists());
426 }
427
428 #[test]
429 fn test_display_trait() {
430 let rel = "display_test.txt";
431 let temp_dir = env::temp_dir().join("app_path_test_display");
432 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
433
434 let expected = temp_dir.join(rel);
435 assert_eq!(format!("{rel_path}"), format!("{}", expected.display()));
436 }
437
438 #[test]
439 fn test_try_from_str() {
440 use std::convert::TryFrom;
441
442 let rel_path = AppPath::try_from("config.toml").unwrap();
443 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
444 let expected = exe_dir.join("config.toml");
445
446 assert_eq!(rel_path.path(), &expected);
447 assert_eq!(rel_path.input(), Path::new("config.toml"));
448 }
449
450 #[test]
451 fn test_try_from_string() {
452 use std::convert::TryFrom;
453
454 let path_string = "data/file.txt".to_string();
455 let rel_path = AppPath::try_from(path_string).unwrap();
456 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
457 let expected = exe_dir.join("data/file.txt");
458
459 assert_eq!(rel_path.path(), &expected);
460 assert_eq!(rel_path.input(), Path::new("data/file.txt"));
461 }
462
463 #[test]
464 fn test_try_from_string_ref() {
465 use std::convert::TryFrom;
466
467 let path_string = "logs/app.log".to_string();
468 let rel_path = AppPath::try_from(&path_string).unwrap();
469 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
470 let expected = exe_dir.join("logs/app.log");
471
472 assert_eq!(rel_path.path(), &expected);
473 assert_eq!(rel_path.input(), Path::new("logs/app.log"));
474 }
475}