tree_type/
lib.rs

1pub mod deps;
2pub mod fs;
3
4use std::io::ErrorKind;
5
6// Re-export the proc macros for user convenience
7pub use tree_type_proc_macro::{dir_type, file_type, tree_type};
8
9pub enum GenericPath {
10    File(GenericFile),
11    Dir(GenericDir),
12}
13
14impl TryFrom<std::fs::DirEntry> for GenericPath {
15    type Error = std::io::Error;
16
17    fn try_from(value: std::fs::DirEntry) -> Result<Self, Self::Error> {
18        let path: &std::path::Path = &value.path();
19        GenericPath::try_from(path)
20    }
21}
22
23#[cfg(feature = "walk")]
24impl TryFrom<crate::deps::walk::DirEntry> for GenericPath {
25    type Error = std::io::Error;
26
27    fn try_from(value: crate::deps::walk::DirEntry) -> Result<Self, Self::Error> {
28        GenericPath::try_from(value.path())
29    }
30}
31
32#[cfg(feature = "enhanced-errors")]
33impl TryFrom<crate::deps::enhanced_errors::DirEntry> for GenericPath {
34    type Error = std::io::Error; // because ::fs_err::Error is private
35
36    fn try_from(value: crate::deps::enhanced_errors::DirEntry) -> Result<Self, Self::Error> {
37        let path: &std::path::Path = &value.path();
38        GenericPath::try_from(path)
39    }
40}
41
42impl TryFrom<&std::path::Path> for GenericPath {
43    type Error = std::io::Error;
44
45    fn try_from(path: &std::path::Path) -> Result<Self, Self::Error> {
46        let path = resolve_path(path)?;
47        if path.is_file() {
48            Ok(GenericPath::File(GenericFile(path)))
49        } else if path.is_dir() {
50            Ok(GenericPath::Dir(GenericDir(path)))
51        } else {
52            Err(std::io::Error::other("unsupported path type"))
53        }
54    }
55}
56
57impl std::fmt::Display for GenericPath {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            GenericPath::File(file) => write!(f, "{}", file.0.display()),
61            GenericPath::Dir(dir) => write!(f, "{}", dir.0.display()),
62        }
63    }
64}
65
66impl std::fmt::Debug for GenericPath {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            GenericPath::File(file) => write!(f, "GenericPath::File({})", file.0.display()),
70            GenericPath::Dir(dir) => write!(f, "GenericPath::Dir({})", dir.0.display()),
71        }
72    }
73}
74
75fn resolve_path(path: &std::path::Path) -> Result<std::path::PathBuf, std::io::Error> {
76    use std::collections::HashSet;
77    let mut p = path.to_path_buf();
78    let mut seen = HashSet::new();
79    seen.insert(p.clone());
80    while p.is_symlink() {
81        p = p.read_link()?;
82        if seen.contains(&p) {
83            return Err(std::io::Error::other("symlink loop"));
84        }
85    }
86    Ok(p)
87}
88
89// Generate GenericFile using the proc macro
90#[allow(unexpected_cfgs)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
93pub struct GenericFile(std::path::PathBuf);
94
95impl GenericFile {
96    /// Create a new wrapper for a file
97    ///
98    /// # Errors
99    ///
100    /// If the file name is invalid. Invalid file name inclued empty file name.
101    pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
102        let path_buf = path.into();
103        let file_name = path_buf.file_name();
104        if file_name.is_none() {
105            return Err(std::io::Error::from(ErrorKind::InvalidFilename));
106        }
107        Ok(Self(path_buf))
108    }
109
110    #[must_use]
111    pub fn as_path(&self) -> &std::path::Path {
112        &self.0
113    }
114
115    #[must_use]
116    pub fn exists(&self) -> bool {
117        self.0.exists()
118    }
119
120    #[must_use]
121    pub fn as_generic(&self) -> GenericFile {
122        self.clone()
123    }
124
125    /// Reads the entire contents of a file into a bytes vector.
126    ///
127    /// # Errors
128    ///
129    /// May return any of the same errors as `std::fs::read`
130    pub fn read(&self) -> std::io::Result<Vec<u8>> {
131        fs::read(&self.0)
132    }
133
134    /// Reads the entire contents of a file into a string.
135    ///
136    /// # Errors
137    ///
138    /// May return any of the same errors as `std::fs::read_to_string`
139    pub fn read_to_string(&self) -> std::io::Result<String> {
140        fs::read_to_string(&self.0)
141    }
142
143    /// Writes a slice as the entire contents of a file.
144    ///
145    /// # Errors
146    ///
147    /// May return any of the same errors as `std::fs::write`
148    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
149        if let Some(parent) = self.0.parent() {
150            if !parent.exists() {
151                fs::create_dir_all(parent)?;
152            }
153        }
154        fs::write(&self.0, contents)
155    }
156
157    /// Create an empty file if it doesn't already exist
158    ///
159    /// # Errors
160    ///  
161    /// May return any of the same errors as `std::fs::open`
162    pub fn create(&self) -> std::io::Result<()> {
163        if let Some(parent) = self.0.parent() {
164            if !parent.exists() {
165                fs::create_dir_all(parent)?;
166            }
167        }
168        fs::create_file(&self.0)
169    }
170
171    /// Removes a file from the filesystem.
172    ///
173    /// # Errors
174    ///
175    /// May return any of the same errors as `std::fs::remove_file`
176    pub fn remove(&self) -> std::io::Result<()> {
177        fs::remove_file(&self.0)
178    }
179
180    /// Given a path, queries the file system to get information about a file, directory, etc.
181    ///
182    /// # Errors
183    ///
184    /// May return any of the same errors as `std::fs::metadata`
185    pub fn fs_metadata(&self) -> std::io::Result<fs::Metadata> {
186        fs::metadata(&self.0)
187    }
188
189    /// Returns the final component of the path as a String.
190    /// See [`std::path::Path::file_name`] for more details.
191    ///
192    #[must_use]
193    #[allow(clippy::missing_panics_doc)]
194    pub fn file_name(&self) -> String {
195        self.0
196            .file_name()
197            .expect("validated in new")
198            .to_string_lossy()
199            .to_string()
200    }
201
202    /// Rename/move this file to a new path.
203    ///
204    /// Consumes the original instance and returns a new instance with the updated path on success.
205    /// On failure, returns both the error and the original instance for recovery.
206    /// Parent directories are created automatically if they don't exist.
207    ///
208    /// # Examples
209    ///
210    /// ```ignore
211    /// let file = GenericFile::new("old.txt");
212    /// match file.rename("new.txt") {
213    ///     Ok(renamed_file) => {
214    ///         // Use renamed_file
215    ///     }
216    ///     Err((error, original_file)) => {
217    ///         // Handle error, original_file is still available
218    ///     }
219    /// }
220    /// ```
221    ///
222    /// # Errors
223    ///  
224    /// May return any of the same errors as `std::fs::rename`
225    pub fn rename(
226        self,
227        new_path: impl AsRef<std::path::Path>,
228    ) -> Result<Self, (std::io::Error, Self)> {
229        let new_path = new_path.as_ref();
230
231        // Create parent directories if they don't exist
232        if let Some(parent) = new_path.parent() {
233            if !parent.exists() {
234                if let Err(e) = fs::create_dir_all(parent) {
235                    return Err((e, self));
236                }
237            }
238        }
239
240        // Attempt the rename
241        match fs::rename(&self.0, new_path) {
242            Ok(()) => Self::new(new_path).map_err(|e| (e, self)),
243            Err(e) => Err((e, self)),
244        }
245    }
246
247    /// Set file permissions to 0o600 (read/write for owner only).
248    ///
249    /// This method is only available on Unix systems.
250    ///
251    /// # Examples
252    ///
253    /// ```ignore
254    /// let file = GenericFile::new("secret.txt");
255    /// file.secure()?;
256    /// ```
257    ///
258    /// # Errors
259    ///  
260    /// May return any of the same errors as `std::fs::set_permissions`
261    #[cfg(unix)]
262    pub fn secure(&self) -> std::io::Result<()> {
263        use std::os::unix::fs::PermissionsExt;
264        let permissions = std::fs::Permissions::from_mode(0o600);
265        fs::set_permissions(&self.0, permissions)
266    }
267
268    /// Get the parent directory as `GenericDir`.
269    /// Files always have a parent directory.
270    #[must_use]
271    #[allow(clippy::missing_panics_doc)]
272    pub fn parent(&self) -> GenericDir {
273        let parent_path = self.0.parent().expect("Files must have a parent directory");
274        GenericDir::new(parent_path).expect("Parent path should be valid")
275    }
276}
277
278impl AsRef<std::path::Path> for GenericFile {
279    fn as_ref(&self) -> &std::path::Path {
280        &self.0
281    }
282}
283
284impl std::fmt::Display for GenericFile {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(f, "{}", self.0.display())
287    }
288}
289
290impl std::fmt::Debug for GenericFile {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        write!(f, "GenericFile({})", self.0.display())
293    }
294}
295
296/// Generic directory type for type-erased directory operations.
297///
298/// This type can be used when you need to work with directories in a generic way,
299/// without knowing their specific type at compile time. It supports conversion
300/// to and from any typed directory struct.
301///
302/// # Examples
303///
304/// ```ignore
305/// use tree_type::{dir_type, GenericDir};
306///
307/// dir_type!(CacheDir);
308///
309/// let cache = CacheDir::new("/var/cache");
310/// let generic: GenericDir = cache.into();
311/// let cache_again = CacheDir::from_generic(generic);
312/// ```
313#[allow(unexpected_cfgs)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
316pub struct GenericDir(std::path::PathBuf);
317
318impl GenericDir {
319    /// Create a new wrapper for a directory
320    ///
321    /// # Errors
322    ///
323    /// If the directory is invalid. Invalid directory includes being empty.
324    pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
325        let path_buf = path.into();
326        // For directories, allow root paths and paths with filename components
327        // Only reject empty paths or invalid paths like ".."
328        if path_buf.as_os_str().is_empty() {
329            return Err(std::io::Error::from(ErrorKind::InvalidFilename));
330        }
331        Ok(Self(path_buf))
332    }
333
334    #[must_use]
335    pub fn as_path(&self) -> &std::path::Path {
336        &self.0
337    }
338
339    #[must_use]
340    pub fn exists(&self) -> bool {
341        self.0.exists()
342    }
343
344    /// Returns a clone of the `GenericDir`
345    #[must_use]
346    pub fn as_generic(&self) -> GenericDir {
347        self.clone()
348    }
349
350    /// Creates a new, empty directory at the provided path
351    ///
352    /// # Errors
353    ///  
354    /// May return any of the same errors as `std::fs::create_dir`
355    pub fn create(&self) -> std::io::Result<()> {
356        fs::create_dir(&self.0)
357    }
358
359    /// Recursively create a directory and all of its parent components if they are missing.
360    ///
361    /// # Errors
362    ///
363    /// May return any of the same errors as `std::fs::create_dir_all`
364    pub fn create_all(&self) -> std::io::Result<()> {
365        fs::create_dir_all(&self.0)
366    }
367
368    /// Removes an empty directory.
369    ///
370    /// # Errors
371    ///
372    /// May return any of the same errors as `std::fs::remove_all`
373    pub fn remove(&self) -> std::io::Result<()> {
374        fs::remove_dir(&self.0)
375    }
376
377    /// Removes a directory at this path, after removing all its contents. Use carefully!
378    ///
379    /// # Errors
380    ///
381    /// May return any of the same errors as `std::fs::remove_dir_all`
382    pub fn remove_all(&self) -> std::io::Result<()> {
383        fs::remove_dir_all(&self.0)
384    }
385
386    /// Returns an iterator over the entries within a directory.
387    ///
388    /// # Errors
389    ///
390    /// May return any of the same errors as `std::fs::read_dir`
391    #[cfg(feature = "enhanced-errors")]
392    pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<GenericPath>>> {
393        crate::deps::enhanced_errors::read_dir(&self.0)
394            .map(|read_dir| read_dir.map(|result| result.and_then(GenericPath::try_from)))
395    }
396
397    /// Returns an iterator over the entries within a directory.
398    ///
399    /// # Errors
400    ///
401    /// May return any of the same errors as `std::fs::read_dir`
402    #[cfg(not(feature = "enhanced-errors"))]
403    pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<GenericPath>>> {
404        std::fs::read_dir(&self.0)
405            .map(|read_dir| read_dir.map(|result| result.and_then(GenericPath::try_from)))
406    }
407
408    /// Given a path, queries the file system to get information about a file, directory, etc.
409    ///
410    /// # Errors
411    ///
412    /// May return any of the same errors as `std::fs::metadata`
413    pub fn fs_metadata(&self) -> std::io::Result<std::fs::Metadata> {
414        fs::metadata(&self.0)
415    }
416
417    #[cfg(feature = "walk")]
418    pub fn walk_dir(&self) -> crate::deps::walk::WalkDir {
419        crate::deps::walk::WalkDir::new(&self.0)
420    }
421
422    #[cfg(feature = "walk")]
423    pub fn walk(&self) -> impl Iterator<Item = std::io::Result<GenericPath>> {
424        crate::deps::walk::WalkDir::new(&self.0)
425            .into_iter()
426            .map(|r| r.map_err(|e| e.into()).and_then(GenericPath::try_from))
427    }
428
429    #[cfg(feature = "walk")]
430    /// Calculate total size in bytes of directory contents
431    pub fn size_in_bytes(&self) -> std::io::Result<u64> {
432        let mut total = 0u64;
433        for entry in crate::deps::walk::WalkDir::new(&self.0) {
434            let entry = entry?;
435            if entry.file_type().is_file() {
436                total += entry.metadata()?.len();
437            }
438        }
439        Ok(total)
440    }
441
442    #[cfg(feature = "walk")]
443    /// List directory contents with metadata
444    pub fn lsl(&self) -> std::io::Result<Vec<(std::path::PathBuf, std::fs::Metadata)>> {
445        let mut results = Vec::new();
446        for entry in crate::deps::walk::WalkDir::new(&self.0) {
447            let entry = entry?;
448            let metadata = entry.metadata()?;
449            results.push((entry.path().to_path_buf(), metadata));
450        }
451        Ok(results)
452    }
453
454    /// Returns the final component of the path as a String.
455    /// For root paths like "/", returns an empty string.
456    /// See [`std::path::Path::file_name`] for more details.
457    #[must_use]
458    pub fn file_name(&self) -> String {
459        self.0
460            .file_name()
461            .map(|name| name.to_string_lossy().to_string())
462            .unwrap_or_default()
463    }
464
465    /// Renames this directory to a new name, replacing the original item if
466    /// `to` already exists.
467    ///
468    /// Consumes the original instance and returns a new instance with the updated path on success.
469    /// On failure, returns both the error and the original instance for recovery.
470    /// Parent directories are created automatically if they don't exist.
471    ///
472    /// # Examples
473    ///
474    /// ```ignore
475    /// let dir = GenericDir::new("old_dir");
476    /// match dir.rename("new_dir") {
477    ///     Ok(renamed_dir) => {
478    ///         // Use renamed_dir
479    ///     }
480    ///     Err((error, original_dir)) => {
481    ///         // Handle error, original_dir is still available
482    ///     }
483    /// }
484    /// ```
485    ///
486    /// # Errors
487    ///  
488    /// May return any of the same errors as `std::fs::rename`
489    pub fn rename(
490        self,
491        new_path: impl AsRef<std::path::Path>,
492    ) -> Result<Self, (std::io::Error, Self)> {
493        let new_path = new_path.as_ref();
494
495        // Create parent directories if they don't exist
496        if let Some(parent) = new_path.parent() {
497            if !parent.exists() {
498                if let Err(e) = std::fs::create_dir_all(parent) {
499                    return Err((e, self));
500                }
501            }
502        }
503
504        // Attempt the rename
505        match fs::rename(&self.0, new_path) {
506            Ok(()) => Self::new(new_path).map_err(|e| (e, self)),
507            Err(e) => Err((e, self)),
508        }
509    }
510
511    /// Set directory permissions to 0o700 (read/write/execute for owner only).
512    ///
513    /// This method is only available on Unix systems.
514    ///
515    /// # Examples
516    ///
517    /// ```ignore
518    /// let dir = GenericDir::new("secret_dir");
519    /// dir.secure()?;
520    /// ```
521    ///
522    /// # Errors
523    ///  
524    /// May return any of the same errors as `std::fs::set_permissions`
525    #[cfg(unix)]
526    pub fn secure(&self) -> std::io::Result<()> {
527        use std::os::unix::fs::PermissionsExt;
528        let permissions = std::fs::Permissions::from_mode(0o700);
529        fs::set_permissions(&self.0, permissions)
530    }
531
532    /// Get the parent directory as `GenericDir`.
533    /// Returns None for directories at the root level.
534    #[must_use]
535    pub fn parent(&self) -> Option<GenericDir> {
536        self.0
537            .parent()
538            .and_then(|parent_path| GenericDir::new(parent_path).ok())
539    }
540}
541
542impl AsRef<std::path::Path> for GenericDir {
543    fn as_ref(&self) -> &std::path::Path {
544        &self.0
545    }
546}
547
548impl std::fmt::Display for GenericDir {
549    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
550        write!(f, "{}", self.0.display())
551    }
552}
553
554impl std::fmt::Debug for GenericDir {
555    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556        write!(f, "GenericDir({})", self.0.display())
557    }
558}
559
560/// Outcome of calling `create_default()` on a file type
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum CreateDefaultOutcome {
563    /// File was created with default content
564    Created,
565    /// File already existed, no action taken
566    AlreadyExists,
567}
568
569/// Error type for `setup()` operations
570#[derive(Debug)]
571pub enum BuildError {
572    /// Error creating a directory
573    Directory(std::path::PathBuf, std::io::Error),
574    /// Error creating a file with default content
575    File(std::path::PathBuf, Box<dyn std::error::Error>),
576}
577
578/// Controls whether validation/ensure operations recurse into child paths
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum Recursive {
581    /// Validate/ensure only this level
582    No,
583    /// Validate/ensure entire tree
584    Yes,
585}
586
587/// Validation error for a specific path
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct ValidationError {
590    /// Path that failed validation
591    pub path: std::path::PathBuf,
592    /// Error message
593    pub message: String,
594}
595
596/// Validation warning for a specific path
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub struct ValidationWarning {
599    /// Path that generated warning
600    pub path: std::path::PathBuf,
601    /// Warning message
602    pub message: String,
603}
604
605/// Result of validation operation
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub struct ValidationReport {
608    /// Validation errors
609    pub errors: Vec<ValidationError>,
610    /// Validation warnings
611    pub warnings: Vec<ValidationWarning>,
612}
613
614impl ValidationReport {
615    /// Create empty validation report
616    #[must_use]
617    pub fn new() -> Self {
618        Self {
619            errors: Vec::new(),
620            warnings: Vec::new(),
621        }
622    }
623
624    /// Check if validation passed (no errors)
625    #[must_use]
626    pub fn is_ok(&self) -> bool {
627        self.errors.is_empty()
628    }
629
630    /// Merge another report into this one
631    pub fn merge(&mut self, other: ValidationReport) {
632        self.errors.extend(other.errors);
633        self.warnings.extend(other.warnings);
634    }
635}
636
637impl Default for ValidationReport {
638    fn default() -> Self {
639        Self::new()
640    }
641}
642
643/// Result returned by validator functions
644#[derive(Debug, Default, Clone, PartialEq, Eq)]
645pub struct ValidatorResult {
646    /// Validation errors
647    pub errors: Vec<String>,
648    /// Validation warnings
649    pub warnings: Vec<String>,
650}