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//! use std::path::PathBuf;
11//!
12//! // Create paths relative to your executable - accepts any path-like type
13//! let config = AppPath::try_new("config.toml")?;
14//! let data = AppPath::try_new("data/users.db")?;
15//!
16//! // Efficient ownership transfer for owned types
17//! let log_file = "logs/app.log".to_string();
18//! let logs = AppPath::try_new(log_file)?; // String is moved
19//!
20//! let path_buf = PathBuf::from("cache/data.bin");
21//! let cache = AppPath::try_new(path_buf)?; // PathBuf is moved
22//!
23//! // Works with any path-like type
24//! let from_path = AppPath::try_new(std::path::Path::new("temp.txt"))?;
25//!
26//! // Alternative: Use TryFrom for string types
27//! let settings = AppPath::try_from("settings.json")?;
28//!
29//! // Absolute paths are used as-is (for system integration)
30//! let system_log = AppPath::try_new("/var/log/app.log")?;
31//! let windows_temp = AppPath::try_new(r"C:\temp\cache.dat")?;
32//!
33//! // Get the paths for use with standard library functions
34//! println!("Config: {}", config.path().display());
35//! println!("Data: {}", data.path().display());
36//!
37//! // Check existence and create directories
38//! if !logs.exists() {
39//! logs.create_dir_all()?;
40//! }
41//!
42//! # Ok::<(), Box<dyn std::error::Error>>(())
43//! ```
44//!
45//! ## Path Resolution Behavior
46//!
47//! `AppPath` intelligently handles different path types:
48//!
49//! - **Relative paths** (e.g., `"config.toml"`, `"data/file.txt"`) are resolved
50//! relative to the executable's directory
51//! - **Absolute paths** (e.g., `"/etc/config"`, `"C:\\temp\\file.txt"`) are used
52//! as-is, ignoring the executable's directory
53//!
54//! This design enables both portable applications and system integration.
55//!
56//! ## Performance
57//!
58//! AppPath is optimized for efficient ownership transfer:
59//!
60//! - **String and PathBuf**: Moved into AppPath (no cloning)
61//! - **Generic types**: Uses `impl Into<PathBuf>` for zero-copy where possible
62//! - **References**: Efficient conversion without unnecessary allocations
63
64use std::env::current_exe;
65use std::path::{Path, PathBuf};
66
67/// Creates paths relative to the executable location for applications.
68///
69/// All files and directories stay together with the executable, making
70/// your application truly portable. Perfect for:
71///
72/// - Portable applications that run from USB drives
73/// - Development tools that should work anywhere
74/// - Corporate environments where you can't install software
75///
76/// # Examples
77///
78/// ```rust
79/// use app_path::AppPath;
80///
81/// // Basic usage
82/// let config = AppPath::try_new("settings.toml")?;
83/// let data_dir = AppPath::try_new("data")?;
84///
85/// // Check if files exist
86/// if config.exists() {
87/// let settings = std::fs::read_to_string(config.path())?;
88/// }
89///
90/// // Create directories
91/// data_dir.create_dir_all()?;
92///
93/// # Ok::<(), Box<dyn std::error::Error>>(())
94/// ```
95#[derive(Clone, Debug)]
96pub struct AppPath {
97 input_path: PathBuf,
98 full_path: PathBuf,
99}
100
101impl AppPath {
102 /// Creates file paths relative to the executable location.
103 ///
104 /// This method accepts any type that can be converted into a `PathBuf`,
105 /// allowing for efficient ownership transfer when possible.
106 ///
107 /// **Behavior with different path types:**
108 /// - **Relative paths** (e.g., `"config.toml"`, `"data/file.txt"`) are resolved
109 /// relative to the executable's directory
110 /// - **Absolute paths** (e.g., `"/etc/config"`, `"C:\\temp\\file.txt"`) are used
111 /// as-is, ignoring the executable's directory
112 ///
113 /// # Arguments
114 ///
115 /// * `path` - A path that will be resolved relative to the executable.
116 /// Can be `&str`, `String`, `&Path`, `PathBuf`, etc.
117 ///
118 /// # Examples
119 ///
120 /// ```rust
121 /// use app_path::AppPath;
122 /// use std::path::PathBuf;
123 ///
124 /// // Relative paths are resolved relative to the executable
125 /// let config = AppPath::try_new("config.toml")?;
126 /// let data = AppPath::try_new("data/users.db")?;
127 ///
128 /// // Absolute paths are used as-is (portable apps usually want relative paths)
129 /// let system_config = AppPath::try_new("/etc/app/config.toml")?;
130 /// let temp_file = AppPath::try_new(r"C:\temp\cache.dat")?;
131 ///
132 /// // From String (moves ownership)
133 /// let filename = "logs/app.log".to_string();
134 /// let logs = AppPath::try_new(filename)?; // filename is moved
135 ///
136 /// // From PathBuf (moves ownership)
137 /// let path_buf = PathBuf::from("cache/data.bin");
138 /// let cache = AppPath::try_new(path_buf)?; // path_buf is moved
139 ///
140 /// # Ok::<(), Box<dyn std::error::Error>>(())
141 /// ```
142 pub fn try_new(path: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
143 let input_path = path.into();
144
145 let exe_dir = current_exe()?
146 .parent()
147 .ok_or_else(|| {
148 std::io::Error::new(
149 std::io::ErrorKind::NotFound,
150 "Could not determine executable parent directory",
151 )
152 })?
153 .to_path_buf();
154
155 let full_path = exe_dir.join(&input_path);
156
157 Ok(Self {
158 input_path,
159 full_path,
160 })
161 }
162
163 /// Override the base directory (useful for testing or custom layouts).
164 ///
165 /// This method allows you to specify a different base directory instead
166 /// of using the executable's directory. Useful for testing or when you
167 /// want a different layout.
168 ///
169 /// # Arguments
170 ///
171 /// * `base` - The new base directory to use
172 ///
173 /// # Examples
174 ///
175 /// ```rust
176 /// use app_path::AppPath;
177 /// use std::env;
178 ///
179 /// let config = AppPath::try_new("config.toml")?
180 /// .with_base(env::temp_dir());
181 ///
182 /// # Ok::<(), Box<dyn std::error::Error>>(())
183 /// ```
184 pub fn with_base(mut self, base: impl AsRef<Path>) -> Self {
185 self.full_path = base.as_ref().join(&self.input_path);
186 self
187 }
188
189 /// Get the original input path (before resolution).
190 ///
191 /// Returns the path as it was originally provided to [`AppPath::try_new`],
192 /// before any resolution or joining with the base directory.
193 ///
194 /// # Examples
195 ///
196 /// ```rust
197 /// use app_path::AppPath;
198 ///
199 /// let app_path = AppPath::try_new("config/settings.toml")?;
200 /// assert_eq!(app_path.input().to_str(), Some("config/settings.toml"));
201 ///
202 /// # Ok::<(), Box<dyn std::error::Error>>(())
203 /// ```
204 #[inline]
205 pub fn input(&self) -> &Path {
206 &self.input_path
207 }
208
209 /// Get the full resolved path.
210 ///
211 /// This is the primary method for getting the actual filesystem path
212 /// where your file or directory is located.
213 ///
214 /// # Examples
215 ///
216 /// ```rust
217 /// use app_path::AppPath;
218 ///
219 /// let config = AppPath::try_new("config.toml")?;
220 ///
221 /// // Get the path for use with standard library functions
222 /// println!("Config path: {}", config.path().display());
223 ///
224 /// // The path is always absolute
225 /// assert!(config.path().is_absolute());
226 ///
227 /// # Ok::<(), Box<dyn std::error::Error>>(())
228 /// ```
229 #[inline]
230 pub fn path(&self) -> &Path {
231 &self.full_path
232 }
233
234 /// Check if the path exists.
235 ///
236 /// # Examples
237 ///
238 /// ```rust
239 /// use app_path::AppPath;
240 ///
241 /// let config = AppPath::try_new("config.toml")?;
242 ///
243 /// if config.exists() {
244 /// println!("Config file found!");
245 /// } else {
246 /// println!("Config file not found, using defaults.");
247 /// }
248 ///
249 /// # Ok::<(), Box<dyn std::error::Error>>(())
250 /// ```
251 #[inline]
252 pub fn exists(&self) -> bool {
253 self.full_path.exists()
254 }
255
256 /// Create parent directories if they don't exist.
257 ///
258 /// This is equivalent to calling [`std::fs::create_dir_all`] on the
259 /// parent directory of this path.
260 ///
261 /// # Examples
262 ///
263 /// ```rust
264 /// use app_path::AppPath;
265 /// use std::env;
266 ///
267 /// // Use a temporary directory for the example
268 /// let temp_dir = env::temp_dir().join("app_path_example");
269 /// let data_file = AppPath::try_new("data/users/profile.json")?
270 /// .with_base(&temp_dir);
271 ///
272 /// // Ensure the "data/users" directory exists
273 /// data_file.create_dir_all()?;
274 ///
275 /// // Verify the directory was created
276 /// assert!(data_file.path().parent().unwrap().exists());
277 ///
278 /// # std::fs::remove_dir_all(&temp_dir).ok();
279 /// # Ok::<(), Box<dyn std::error::Error>>(())
280 /// ```
281 pub fn create_dir_all(&self) -> std::io::Result<()> {
282 if let Some(parent) = self.full_path.parent() {
283 std::fs::create_dir_all(parent)?;
284 }
285 Ok(())
286 }
287}
288
289impl std::fmt::Display for AppPath {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 write!(f, "{}", self.full_path.display())
292 }
293}
294
295impl From<AppPath> for PathBuf {
296 #[inline]
297 fn from(app_path: AppPath) -> Self {
298 app_path.full_path
299 }
300}
301impl AsRef<Path> for AppPath {
302 #[inline]
303 fn as_ref(&self) -> &Path {
304 self.full_path.as_ref()
305 }
306}
307
308/// Ergonomic fallible conversion from string types.
309///
310/// These implementations allow you to create `AppPath` instances directly
311/// from strings, with proper error handling for the fallible operation.
312/// For other path types like `PathBuf`, use [`AppPath::try_new`] directly.
313///
314/// # Examples
315///
316/// ```rust
317/// use app_path::AppPath;
318/// use std::convert::TryFrom;
319/// use std::path::PathBuf;
320///
321/// // From &str
322/// let config = AppPath::try_from("config.toml")?;
323///
324/// // From String (moves ownership)
325/// let data_file = "data/users.db".to_string();
326/// let data = AppPath::try_from(data_file)?;
327///
328/// // For PathBuf, use try_new directly (moves ownership)
329/// let path_buf = PathBuf::from("logs/app.log");
330/// let logs = AppPath::try_new(path_buf)?;
331///
332/// # Ok::<(), Box<dyn std::error::Error>>(())
333/// ```
334impl TryFrom<&str> for AppPath {
335 type Error = std::io::Error;
336
337 fn try_from(path: &str) -> Result<Self, Self::Error> {
338 AppPath::try_new(path)
339 }
340}
341
342impl TryFrom<String> for AppPath {
343 type Error = std::io::Error;
344
345 fn try_from(path: String) -> Result<Self, Self::Error> {
346 AppPath::try_new(path)
347 }
348}
349
350impl TryFrom<&String> for AppPath {
351 type Error = std::io::Error;
352
353 fn try_from(path: &String) -> Result<Self, Self::Error> {
354 AppPath::try_new(path)
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use std::env;
362 use std::fs::{self, File};
363 use std::io::Write;
364 use std::path::Path;
365
366 /// Helper to create a file at a given path for testing.
367 fn create_test_file(path: &Path) {
368 if let Some(parent) = path.parent() {
369 fs::create_dir_all(parent).unwrap();
370 }
371 let mut file = File::create(path).unwrap();
372 writeln!(file, "test").unwrap();
373 }
374
375 #[test]
376 fn resolves_relative_path_to_exe_dir() {
377 // Simulate a file relative to the executable
378 let rel = "myconfig.toml";
379 let rel_path = AppPath::try_new(rel).unwrap();
380 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
381 let expected = exe_dir.join(rel);
382
383 assert_eq!(rel_path.path(), &expected);
384 assert!(rel_path.path().is_absolute());
385 }
386
387 #[test]
388 fn resolves_relative_path_with_custom_base() {
389 // Use a temp directory as the base
390 let temp_dir = env::temp_dir().join("app_path_test_base");
391 let _ = fs::remove_dir_all(&temp_dir);
392 fs::create_dir_all(&temp_dir).unwrap();
393
394 let rel = "subdir/file.txt";
395 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
396 let expected = temp_dir.join(rel);
397
398 assert_eq!(rel_path.path(), &expected);
399 assert!(rel_path.path().is_absolute());
400 }
401
402 #[test]
403 fn can_access_file_using_full_path() {
404 // Actually create a file and check that the full path points to it
405 let temp_dir = env::temp_dir().join("app_path_test_access");
406 let file_name = "access.txt";
407 let file_path = temp_dir.join(file_name);
408 let _ = fs::remove_dir_all(&temp_dir);
409 fs::create_dir_all(&temp_dir).unwrap();
410 create_test_file(&file_path);
411
412 let rel_path = AppPath::try_new(file_name).unwrap().with_base(&temp_dir);
413 assert!(rel_path.exists());
414 assert_eq!(rel_path.path(), &file_path);
415 }
416
417 #[test]
418 fn handles_dot_and_dotdot_components() {
419 let temp_dir = env::temp_dir().join("app_path_test_dot");
420 let _ = fs::remove_dir_all(&temp_dir);
421 fs::create_dir_all(&temp_dir).unwrap();
422
423 let rel = "./foo/../bar.txt";
424 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
425 let expected = temp_dir.join(rel);
426
427 assert_eq!(rel_path.path(), &expected);
428 }
429
430 #[test]
431 fn as_ref_and_into_pathbuf_are_consistent() {
432 let rel = "somefile.txt";
433 let rel_path = AppPath::try_new(rel).unwrap();
434 let as_ref_path: &Path = rel_path.as_ref();
435 let into_pathbuf: PathBuf = rel_path.clone().into();
436 assert_eq!(as_ref_path, into_pathbuf.as_path());
437 }
438
439 #[test]
440 fn test_input_method() {
441 let rel = "config/app.toml";
442 let rel_path = AppPath::try_new(rel).unwrap();
443 assert_eq!(rel_path.input(), Path::new(rel));
444 }
445
446 #[test]
447 fn test_path_method() {
448 let rel = "data/file.txt";
449 let temp_dir = env::temp_dir().join("app_path_test_full");
450 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
451 assert_eq!(rel_path.path(), temp_dir.join(rel));
452 }
453
454 #[test]
455 fn test_exists_method() {
456 let temp_dir = env::temp_dir().join("app_path_test_exists");
457 let _ = fs::remove_dir_all(&temp_dir);
458 fs::create_dir_all(&temp_dir).unwrap();
459
460 let file_name = "exists_test.txt";
461 let file_path = temp_dir.join(file_name);
462 create_test_file(&file_path);
463
464 let rel_path = AppPath::try_new(file_name).unwrap().with_base(&temp_dir);
465 assert!(rel_path.exists());
466
467 let non_existent = AppPath::try_new("non_existent.txt")
468 .unwrap()
469 .with_base(&temp_dir);
470 assert!(!non_existent.exists());
471 }
472
473 #[test]
474 fn test_create_dir_all() {
475 let temp_dir = env::temp_dir().join("app_path_test_create");
476 let _ = fs::remove_dir_all(&temp_dir);
477
478 let rel = "deep/nested/dir/file.txt";
479 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
480
481 rel_path.create_dir_all().unwrap();
482 assert!(rel_path.path().parent().unwrap().exists());
483 }
484
485 #[test]
486 fn test_display_trait() {
487 let rel = "display_test.txt";
488 let temp_dir = env::temp_dir().join("app_path_test_display");
489 let rel_path = AppPath::try_new(rel).unwrap().with_base(&temp_dir);
490
491 let expected = temp_dir.join(rel);
492 assert_eq!(format!("{rel_path}"), format!("{}", expected.display()));
493 }
494
495 #[test]
496 fn test_try_from_str() {
497 use std::convert::TryFrom;
498
499 let rel_path = AppPath::try_from("config.toml").unwrap();
500 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
501 let expected = exe_dir.join("config.toml");
502
503 assert_eq!(rel_path.path(), &expected);
504 assert_eq!(rel_path.input(), Path::new("config.toml"));
505 }
506
507 #[test]
508 fn test_try_from_string() {
509 use std::convert::TryFrom;
510
511 let path_string = "data/file.txt".to_string();
512 let rel_path = AppPath::try_from(path_string).unwrap();
513 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
514 let expected = exe_dir.join("data/file.txt");
515
516 assert_eq!(rel_path.path(), &expected);
517 assert_eq!(rel_path.input(), Path::new("data/file.txt"));
518 }
519
520 #[test]
521 fn test_try_from_string_ref() {
522 use std::convert::TryFrom;
523
524 let path_string = "logs/app.log".to_string();
525 let rel_path = AppPath::try_from(&path_string).unwrap();
526 let exe_dir = current_exe().unwrap().parent().unwrap().to_path_buf();
527 let expected = exe_dir.join("logs/app.log");
528
529 assert_eq!(rel_path.path(), &expected);
530 assert_eq!(rel_path.input(), Path::new("logs/app.log"));
531 }
532
533 #[test]
534 fn test_try_new_with_different_types() {
535 use std::path::PathBuf;
536
537 // Test various input types with try_new
538 let from_str = AppPath::try_new("test.txt").unwrap();
539 let from_string = AppPath::try_new("test.txt".to_string()).unwrap();
540 let from_path_buf = AppPath::try_new(PathBuf::from("test.txt")).unwrap();
541 let from_path_ref = AppPath::try_new(Path::new("test.txt")).unwrap();
542
543 // All should produce equivalent results
544 assert_eq!(from_str.input(), from_string.input());
545 assert_eq!(from_string.input(), from_path_buf.input());
546 assert_eq!(from_path_buf.input(), from_path_ref.input());
547 assert_eq!(from_str.input(), Path::new("test.txt"));
548 }
549
550 #[test]
551 fn test_ownership_transfer() {
552 use std::path::PathBuf;
553
554 let path_buf = PathBuf::from("test.txt");
555 let app_path = AppPath::try_new(path_buf).unwrap();
556 // path_buf is moved and no longer accessible
557
558 assert_eq!(app_path.input(), Path::new("test.txt"));
559
560 // Test with String too
561 let string_path = "another_test.txt".to_string();
562 let app_path2 = AppPath::try_new(string_path).unwrap();
563 // string_path is moved and no longer accessible
564
565 assert_eq!(app_path2.input(), Path::new("another_test.txt"));
566 }
567
568 #[test]
569 fn test_absolute_path_behavior() {
570 // Test what happens with an absolute path
571 let absolute_path = if cfg!(windows) {
572 r"C:\temp\config.toml"
573 } else {
574 "/tmp/config.toml"
575 };
576
577 let app_path = AppPath::try_new(absolute_path).unwrap();
578
579 // PathBuf::join() has special behavior: when joining with an absolute path,
580 // the absolute path replaces the base path entirely
581 // So AppPath correctly handles absolute paths by using them as-is
582 assert_eq!(app_path.path(), Path::new(absolute_path));
583 assert_eq!(app_path.input(), Path::new(absolute_path));
584
585 println!("Input: {absolute_path}");
586 println!("Result: {}", app_path.path().display());
587
588 // Verify it's still an absolute path
589 assert!(app_path.path().is_absolute());
590 }
591
592 #[test]
593 fn test_pathbuf_join_behavior_with_absolute_paths() {
594 use std::path::PathBuf;
595
596 // Let's understand how PathBuf::join works with absolute paths
597 let base = PathBuf::from("/home/user");
598 let absolute = PathBuf::from("/etc/config.toml");
599 let relative = PathBuf::from("config.toml");
600
601 println!("Base: {}", base.display());
602 println!("Relative join: {}", base.join(&relative).display());
603 println!("Absolute join: {}", base.join(&absolute).display());
604
605 // PathBuf::join has special behavior for absolute paths:
606 // If the right-hand side is absolute, it replaces the left-hand side
607 assert_eq!(
608 base.join(&relative),
609 PathBuf::from("/home/user/config.toml")
610 );
611 assert_eq!(base.join(&absolute), PathBuf::from("/etc/config.toml"));
612
613 // Same on Windows
614 if cfg!(windows) {
615 let win_base = PathBuf::from(r"C:\Users\User");
616 let win_absolute = PathBuf::from(r"D:\temp\file.txt");
617 let win_relative = PathBuf::from("file.txt");
618
619 assert_eq!(
620 win_base.join(&win_relative),
621 PathBuf::from(r"C:\Users\User\file.txt")
622 );
623 assert_eq!(
624 win_base.join(&win_absolute),
625 PathBuf::from(r"D:\temp\file.txt")
626 );
627 }
628 }
629}