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}