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}