tree_type/
lib.rs

1//! Type-safe path navigation macros for Rust projects with fixed directory structures.
2//!
3//! # Quick Example
4//!
5//! Basic usage example showing type-safe navigation and setup.
6//!
7//! ```
8//! use tree_type::tree_type;
9//! # use tempfile::TempDir;
10//!
11//! // Define your directory structure
12//! tree_type! {
13//!     ProjectRoot {
14//!         src/ {
15//!             lib("lib.rs"),
16//!             main("main.rs")
17//!         },
18//!         target/,
19//!         readme("README.md")
20//!     }
21//! }
22//!
23//! // Each path gets its own type (which cna be overridden)
24//! fn process_source(src: &ProjectRootSrc) -> std::io::Result<()> {
25//!     let lib_rs_file = src.lib();      // ProjectRootSrcLib
26//!     let main_rs_file = src.main();    // ProjectRootSrcMain
27//!     Ok(())
28//! }
29//!
30//! # fn main() -> std::io::Result<()> {
31//! # let temp_dir = TempDir::new()?;
32//! # let project_root = temp_dir.path().join("project");
33//! let project = ProjectRoot::new(project_root)?;
34//! let src = project.src();           // ProjectRootSrc
35//! let readme = project.readme();     // ProjectRootReadme
36//!
37//! process_source(&src)?;
38//! // process_source(&readme)?;       // would be a compilation error
39//!
40//! // Setup entire structure
41//! project.setup();                   // Creates src/, target/, and all files
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Overview
47//!
48//! `tree-type` provides macros for creating type-safe filesystem path types:
49//!
50//! - `tree_type!` - Define a tree of path types with automatic navigation methods
51//! - `dir_type!` - Convenience wrapper for simple directory types (no children)
52//! - `file_type!` - Convenience macro for simple file types (single file with operations)
53//!
54//! # Features
55//!
56//! - **Type Safety**: Each path in your directory structure gets its own type
57//! - **Navigation Methods**: Automatic generation of navigation methods
58//! - **Custom Names**: Support for custom type names and filenames
59//! - **Dynamic IDs**: ID-based navigation for dynamic directory structures
60//! - **Rich Operations**: Built-in filesystem operations (create, read, write, remove, etc.)
61//!
62//! # Installation
63//!
64//! Add to your `Cargo.toml`:
65//!
66//! ```toml
67//! [dependencies]
68//! tree-type = "0.1.0"
69//! ```
70//!
71//! ## Features
72//!
73//! All features are opt-in to minimize dependencies:
74//!
75//! | feature | description | dependencies |
76//! |---|---|---|
77//! | `serde` |Adds Serialize/Deserialize derives to all path types | `serde` |
78//! | `enhanced-errors` | Enhanced error messages for filesystem operations | `fs-err`, `path_facts` |
79//! | `walk` | Directory traversal methods | `walkdir` |
80//! | `pattern-validation` | Regex pattern validation for dynamic ID blocks | `regex` |
81//!
82//! ```toml
83//! # With all features
84//! [dependencies]
85//! tree-type = { version = "0.1.0", features = ["serde", "enhanced-errors", "walk", "pattern-validation"] }
86//! ```
87//!
88//! # Usage Examples
89//!
90//! ## Basic Tree Structure
91//!
92//! Specify the type that represent the root of your tree, it will be a directory. Then within
93//! `{}` specify the identifiers of the files and directories that are children of the
94//! root. Directories as identified by having a trailing `/` after their identifier, otherwise
95//! they are files.
96//!
97//! ```
98//! use tree_type::tree_type;
99//! # use tempfile::TempDir;
100//!
101//! tree_type! {
102//!     ProjectRoot {
103//!         src/,
104//!         target/,
105//!         readme("README.md")
106//!     }
107//! }
108//!
109//! # fn main() -> std::io::Result<()> {
110//! # let temp_dir = TempDir::new()?;
111//! # let project_root = temp_dir.path().join("project");
112//! let project = ProjectRoot::new(project_root.clone())?;
113//! let src = project.src();           // ProjectRootSrc
114//! let readme = project.readme();     // ProjectRootReadme
115//!
116//! assert_eq!(src.as_path(), project_root.join("src"));
117//! assert_eq!(readme.as_path(), project_root.join("README.md"));
118//! # Ok(())
119//! # }
120//! ```
121//!
122//! ## Custom Filenames
123//!
124//! By default the filename will be the same as the identifier, (as long is it is valid for
125//! the filesystem).
126//!
127//! To specify an alternative filename, e.g. one where the filename isn't a valid Rust
128//! identifier, specify the filename as `identifier/("file-name")`. Note that the directory
129//! indicator (`/`) comes after the identifier, not the directory name.
130//!
131//! ```
132//! use tree_type::tree_type;
133//! # use tempfile::TempDir;
134//!
135//! tree_type! {
136//!     UserHome {
137//!         ssh/(".ssh") {
138//!             ecdsa_public("id_ecdsa.pub"),
139//!             ed25519_public("id_ed25519.pub")
140//!         }
141//!     }
142//! }
143//!
144//! # fn main() -> std::io::Result<()> {
145//! # let temp_dir = TempDir::new()?;
146//! # let home_dir = temp_dir.path().join("home");
147//! let home = UserHome::new(home_dir.clone())?;
148//! let ssh = home.ssh();                    // UserHomeSsh (maps to .ssh)
149//! let key = ssh.ecdsa_public();            // UserHomeSshEcdsaPublic (maps to id_ecdsa.pub)
150//!
151//! assert_eq!(ssh.as_path(), home_dir.join(".ssh"));
152//! assert_eq!(key.as_path(), home_dir.join(".ssh/id_ecdsa.pub"));
153//! # Ok(())
154//! # }
155//! ```
156//!
157//! ## Custom Type Names
158//!
159//! `tree_type` will generate type names for each file and directory by appending the capitalised
160//! identifier to the parent type, unless you override this with `as`.
161//!
162//! ```
163//! use tree_type::tree_type;
164//! # use tempfile::TempDir;
165//!
166//! tree_type! {
167//!     ProjectRoot {
168//!         src/ {              // as ProjectRootSrc
169//!             main("main.rs") // as ProjectRootSrcMain
170//!         },
171//!         readme("README.md") as ReadmeFile // default would have been ProjectRootReadme
172//!     }
173//! }
174//!
175//! # fn main() -> std::io::Result<()> {
176//! # let temp_dir = TempDir::new()?;
177//! # let project_dir = temp_dir.path().join("project");
178//! let project = ProjectRoot::new(project_dir)?;
179//!
180//! let src: ProjectRootSrc = project.src();
181//! let main: ProjectRootSrcMain = src.main();
182//! let readme: ReadmeFile = project.readme();
183//! # Ok(())
184//! }
185//! ```
186//!
187//! ## Default File Content
188//!
189//! Create files with default content if they don't exist:
190//!
191//! ```
192//! use tree_type::{tree_type, CreateDefaultOutcome};
193//! # use tempfile::TempDir;
194//!
195//! fn default_config(file: &ProjectRootConfig) -> Result<String, std::io::Error> {
196//!     Ok(format!("# Config for {}\n", file.as_path().display()))
197//! }
198//!
199//! tree_type! {
200//!     ProjectRoot {
201//!         #[default("CHANGELOG\n")]
202//!         changelog("CHANGELOG"),
203//!
204//!         #[default("# My Project\n")] // create file with the string as content
205//!         readme("README.md"),
206//!
207//!         #[default = default_config]
208//!         config("config.toml"),
209//!     }
210//! }
211//!
212//! # fn main() -> std::io::Result<()> {
213//! # let temp_dir = TempDir::new()?;
214//! # let project_path = temp_dir.path();
215//! let project = ProjectRoot::new(project_path)?;
216//!
217//! let changelog = project.changelog();
218//! let readme = project.readme();
219//! let config = project.config();
220//!
221//! changelog.write("existing content")?;
222//!
223//! assert!(changelog.exists()); // an existing file
224//!
225//! assert!(!readme.exists());   // don't exist yet
226//! assert!(!config.exists());
227//!
228//! match project.setup() {
229//!     Ok(_) => println!("Project structure created successfully"),
230//!     Err(errors) => {
231//!         for error in errors {
232//!             match error {
233//!                 tree_type::BuildError::Directory(path, e) => eprintln!("Dir error at {:?}: {}", path, e),
234//!                 tree_type::BuildError::File(path, e) => eprintln!("File error at {:?}: {}", path, e),
235//!             }
236//!         }
237//!     }
238//! }
239//! assert!(readme.exists());    // created and set to default content
240//! assert_eq!(readme.read_to_string()?, "# My Project\n");
241//!
242//! assert!(config.exists());    // created and function sets the content
243//! assert!(config.read_to_string()?.starts_with("# Config for "));
244//!
245//! assert!(changelog.exists()); // existing file is left unchanged
246//! assert_eq!(changelog.read_to_string()?, "existing content");
247//! # Ok(())
248//! # }
249//! ```
250//! The function `f` in `#[default = f]`:
251//!
252//! - Takes `&FileType` as parameter (self-aware, can access own path)
253//! - Returns `Result<String, E>` where E can be any error type
254//! - Allows network requests, file I/O, parsing, etc.
255//! - Errors are propagated to the caller
256//!
257//! The `setup()` method:
258//!
259//! - Creates all directories in the tree
260//! - Creates all files with a `#[default = function]` attribute
261//! - Collects all errors and continues processing (doesn't fail fast)
262//! - Returns `Result<(), Vec<BuildError>>` with all errors if any occurred
263//! - Skips files that already exist (won't overwrite)
264//!
265//! ### Dynamic ID
266//!
267//! Dynamic ID support allows you to define parameterized paths in your directory
268//! structure where the actual directory/file names are determined at runtime using
269//! ID parameters.
270//!
271//! ```
272//! use tree_type::tree_type;
273//! use tree_type::ValidatorResult;
274//! # use tempfile::TempDir;
275//!
276//! fn is_valid_log_name(log_file: &LogFile) -> ValidatorResult {
277//!     let mut result = ValidatorResult::default();
278//!     let file_name = log_file.file_name();
279//!     if !file_name.starts_with("log-") {
280//!         result.errors.push(format!("log_file name '{file_name}' must start with 'log-'"));
281//!     }
282//!     result
283//! }
284//!
285//! tree_type! {
286//!     Root {
287//!         users/ {
288//!             [user_id: String]/ as UserDir {  // Dynamic directory
289//!                 #[required]
290//!                 #[default("{}")]
291//!                 profile("profile.json"),
292//!                 settings("settings.toml"),
293//!                 posts/ {
294//!                     [post_id: u32] as PostFile // nested dynamic
295//!                 }
296//!             }
297//!         },
298//!         logs/ {
299//!             #[validate(is_valid_log_name)]
300//!             [log_name: String] as LogFile    // Dynamic file (no trailing slash)
301//!         }
302//!     }
303//! }
304//!
305//! # fn main() -> std::io::Result<()> {
306//! # let temp_dir = TempDir::new()?;
307//! # let root_dir = temp_dir.path().join("root");
308//! let root = Root::new(root_dir.clone())?;
309//!
310//! let user_dir: UserDir = root.users().user_id("42");
311//! let _result = user_dir.setup();
312//! assert_eq!(user_dir.as_path(), root_dir.join("users/42"));
313//! assert!(user_dir.profile().exists()); // required + default
314//! assert!(!user_dir.settings().exists()); // not required and/or no default
315//! assert_eq!(user_dir.settings().as_path(), root_dir.join("users/42/settings.toml"));
316//!
317//! let log_file = root.logs().log_name("foo.log");
318//! assert_eq!(log_file.as_path(), root_dir.join("logs/foo.log"));
319//! assert!(!log_file.exists()); // we need to create this ourselves
320//!
321//! // FIXME: can't validate a filename until the file exists
322//! log_file.write("bar")?;
323//!
324//! // validation fails because `foo.log` doesn't start with `log-`
325//! // FIXME: `validate()` should return a `Result<T, E>` rather then a ValidationReport
326//! let report = root.logs().validate();
327//! assert!(!report.is_ok());
328//! assert_eq!(report.errors.len(), 1);
329//! assert!(report.errors[0].message.contains("must start with 'log-'"));
330//! # Ok(())
331//! # }
332//! ```
333//!
334//! # File Type Macro
335//!
336//! The `file_type` macro provides for when you only need to work with a single file rather
337//! than a directory structure. You would use it instead of `tree_type` when you only need to
338//! manage one file, not a directory tree, or when you need to treat several files in a directory
339//! tree in a more generic way.
340//!
341//! The `tree_type` macro uses the `file_type` macro to represent any files defined in it.
342//!
343//! ```
344//! use tree_type::file_type;
345//!
346//! file_type!(ConfigFile);
347//!
348//! # fn main() -> std::io::Result<()> {
349//! # let temp_dir = tempfile::TempDir::new()?;
350//! # let root_dir = temp_dir.path().join("root");
351//! let config_file = ConfigFile::new(root_dir.join("config.toml"))?;
352//!
353//! config_file.write("# new config file")?;
354//! assert!(config_file.exists());
355//! let config = config_file.read_to_string()?;
356//! assert_eq!(config, "# new config file");
357//! # Ok(())
358//! # }
359//! ```
360//!
361//! ## File Operations
362//!
363//! File types support:
364//!
365//! - `display()` - Get Display object for formatting paths
366//! - `read_to_string()` - Read file as string
367//! - `read()` - Read file as bytes
368//! - `write()` - Write content to file
369//! - `create_default()` - Create file with default content if it doesn't exist (requires #[default = function] or #[default = "content"] attribute)
370//! - `exists()` - Check if file exists
371//! - `remove()` - Delete file
372//! - `fs_metadata()` - Get file metadata
373//! - `secure()` (Unix only) - Set permissions to 0o600
374//!
375//! ```
376//! use tree_type::tree_type;
377//! # use tempfile::TempDir;
378//!
379//! tree_type! {
380//!     ProjectRoot {
381//!         readme("README.md") as Readme
382//!     }
383//! }
384//!
385//! # fn main() -> std::io::Result<()> {
386//! # let temp_dir = TempDir::new()?;
387//! # let project_dir = temp_dir.path().join("project");
388//! let project = ProjectRoot::new(project_dir)?;
389//! let readme = project.readme();
390//!
391//! // Write content to file
392//! readme.write("# Hello World")?;
393//!
394//! assert!(readme.exists());
395//!
396//! // Read content back
397//! let content = readme.read_to_string()?;
398//! assert_eq!(content, "# Hello World");
399//! # Ok(())
400//! # }
401//! ```
402//!
403//! # Directory Type Macro
404//!
405//! The `dir_type` macro provides for when you only need to work with a single directory rather
406//! than a nested directory structure. You would use it instead of `tree_type` when you only need
407//! to manage one directory, not a directory tree, or when you need to treat several directories
408//! in a more generic way.
409//!
410//! The `tree_type` macro uses the `dir_type` macro to represent the directories defined in it.
411//!
412//! ```
413//! use tree_type::dir_type;
414//!
415//! dir_type!(ConfigDir);
416//!
417//! fn handle_config(dir: &ConfigDir) -> std::io::Result<()> {
418//!     if dir.exists() {
419//!         // ...
420//!     } else {
421//!         // ...
422//!     }
423//!     Ok(())
424//! }
425//!
426//! # fn main() -> std::io::Result<()> {
427//! # let temp_dir = tempfile::TempDir::new()?;
428//! # let root_dir = temp_dir.path().join("root");
429//! let config_dir = ConfigDir::new(root_dir.join("config"))?;
430//!
431//! config_dir.create_all()?;
432//! handle_config(&config_dir)?;
433//! # Ok(())
434//! # }
435//! ```
436//!
437//! ## Directory Operations
438//!
439//! Directory types support:
440//!
441//! -  display() - Get Display object for formatting paths
442//! -  create_all() - Create directory and parents
443//! -  create() - Create directory (parent must exist)
444//! -  setup() - Create directory and all child directories/files recursively
445//! -  validate(recursive) - Validate tree structure without creating anything
446//! -  ensure(recursive) - Validate and create missing required paths
447//! -  exists() - Check if directory exists
448//! -  read_dir() - List directory contents
449//! -  remove() - Remove empty directory
450//! -  remove_all() - Remove directory recursively
451//! -  fs_metadata() - Get directory metadata
452//!
453//! With `walk` feature enabled:
454//!
455//! -  walk_dir() - Walk directory tree (returns iterator)
456//! -  walk() - Walk with callbacks for dirs/files
457//! -  size_in_bytes() - Calculate total size recursively
458//!
459//! # Symbolic links
460//!
461//! Create (soft) symbolic links to other files or directories in the tree.
462//!
463//! This feature is only available on unix-like environments (i.e. `#[cfg(unix)]`).
464//!
465//! ```
466//! use tree_type::tree_type;
467//! # use tempfile::TempDir;
468//!
469//! tree_type! {
470//!     App {
471//!         config/ {
472//!             #[default("production settings")]
473//!             production("prod.toml"),
474//!    
475//!             #[default("staging settings")]
476//!             staging("staging.toml"),
477//!             
478//!             #[default("development settings")]
479//!             development("dev.toml"),
480//!             
481//!             #[symlink = production] // sibling
482//!             active("active.toml")
483//!         },
484//!         data/ {
485//!             #[symlink = /config/production] // cross-directory
486//!             config("config.toml"),
487//!         }
488//!     }
489//! }
490//!
491//! # fn main() -> std::io::Result<()> {
492//! # let temp_dir = TempDir::new()?;
493//! # let app_path = temp_dir.path().join("app");
494//! let app = App::new(app_path)?;
495//!
496//! let _result = app.setup();
497//!
498//! assert!(app.config().active().exists());
499//! assert!(app.data().config().exists());
500//!
501//! // /config/active.toml -> /config/prod.toml
502//! assert_eq!(app.config().active().read_to_string()?, "production settings");
503//! // /data/config -> /config/active.toml -> /config/prod.toml
504//! assert_eq!(app.data().config().read_to_string()?, "production settings");
505//! # Ok(())
506//! # }
507//! ```
508//!
509//! Symlink targets must exist, so the target should have a `#[required]` attribute for
510//! directories, or `#[default...]` attribute for files.
511//!
512//! # Contributing
513//!
514//! Contributions are welcome! Please feel free to submit a Pull Request at https://codeberg.org/kemitix/tree-type/issues
515//!
516
517pub mod deps;
518pub mod fs;
519
520use std::io::ErrorKind;
521
522// Re-export the proc macros for user convenience
523pub use tree_type_proc_macro::{dir_type, file_type, tree_type};
524
525pub enum GenericPath {
526    File(GenericFile),
527    Dir(GenericDir),
528}
529
530impl TryFrom<std::fs::DirEntry> for GenericPath {
531    type Error = std::io::Error;
532
533    fn try_from(value: std::fs::DirEntry) -> Result<Self, Self::Error> {
534        let path: &std::path::Path = &value.path();
535        GenericPath::try_from(path)
536    }
537}
538
539#[cfg(feature = "walk")]
540impl TryFrom<crate::deps::walk::DirEntry> for GenericPath {
541    type Error = std::io::Error;
542
543    fn try_from(value: crate::deps::walk::DirEntry) -> Result<Self, Self::Error> {
544        GenericPath::try_from(value.path())
545    }
546}
547
548#[cfg(feature = "enhanced-errors")]
549impl TryFrom<crate::deps::enhanced_errors::DirEntry> for GenericPath {
550    type Error = std::io::Error; // because ::fs_err::Error is private
551
552    fn try_from(value: crate::deps::enhanced_errors::DirEntry) -> Result<Self, Self::Error> {
553        let path: &std::path::Path = &value.path();
554        GenericPath::try_from(path)
555    }
556}
557
558impl TryFrom<&std::path::Path> for GenericPath {
559    type Error = std::io::Error;
560
561    fn try_from(path: &std::path::Path) -> Result<Self, Self::Error> {
562        let path = resolve_path(path)?;
563        if path.is_file() {
564            Ok(GenericPath::File(GenericFile(path)))
565        } else if path.is_dir() {
566            Ok(GenericPath::Dir(GenericDir(path)))
567        } else {
568            Err(std::io::Error::other("unsupported path type"))
569        }
570    }
571}
572
573impl std::fmt::Display for GenericPath {
574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575        match self {
576            GenericPath::File(file) => write!(f, "{}", file.0.display()),
577            GenericPath::Dir(dir) => write!(f, "{}", dir.0.display()),
578        }
579    }
580}
581
582impl std::fmt::Debug for GenericPath {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        match self {
585            GenericPath::File(file) => write!(f, "GenericPath::File({})", file.0.display()),
586            GenericPath::Dir(dir) => write!(f, "GenericPath::Dir({})", dir.0.display()),
587        }
588    }
589}
590
591fn resolve_path(path: &std::path::Path) -> Result<std::path::PathBuf, std::io::Error> {
592    use std::collections::HashSet;
593    let mut p = path.to_path_buf();
594    let mut seen = HashSet::new();
595    seen.insert(p.clone());
596    while p.is_symlink() {
597        p = p.read_link()?;
598        if seen.contains(&p) {
599            return Err(std::io::Error::other("symlink loop"));
600        }
601    }
602    Ok(p)
603}
604
605// Generate GenericFile using the proc macro
606#[allow(unexpected_cfgs)]
607#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
608#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
609pub struct GenericFile(std::path::PathBuf);
610
611impl GenericFile {
612    /// Create a new wrapper for a file
613    ///
614    /// # Errors
615    ///
616    /// If the file name is invalid. Invalid file name inclued empty file name.
617    pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
618        let path_buf = path.into();
619        let file_name = path_buf.file_name();
620        if file_name.is_none() {
621            return Err(std::io::Error::from(ErrorKind::InvalidFilename));
622        }
623        Ok(Self(path_buf))
624    }
625
626    #[must_use]
627    pub fn as_path(&self) -> &std::path::Path {
628        &self.0
629    }
630
631    #[must_use]
632    pub fn exists(&self) -> bool {
633        self.0.exists()
634    }
635
636    #[must_use]
637    pub fn as_generic(&self) -> GenericFile {
638        self.clone()
639    }
640
641    /// Reads the entire contents of a file into a bytes vector.
642    ///
643    /// # Errors
644    ///
645    /// May return any of the same errors as `std::fs::read`
646    pub fn read(&self) -> std::io::Result<Vec<u8>> {
647        fs::read(&self.0)
648    }
649
650    /// Reads the entire contents of a file into a string.
651    ///
652    /// # Errors
653    ///
654    /// May return any of the same errors as `std::fs::read_to_string`
655    pub fn read_to_string(&self) -> std::io::Result<String> {
656        fs::read_to_string(&self.0)
657    }
658
659    /// Writes a slice as the entire contents of a file.
660    ///
661    /// # Errors
662    ///
663    /// May return any of the same errors as `std::fs::write`
664    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
665        if let Some(parent) = self.0.parent() {
666            if !parent.exists() {
667                fs::create_dir_all(parent)?;
668            }
669        }
670        fs::write(&self.0, contents)
671    }
672
673    /// Create an empty file if it doesn't already exist
674    ///
675    /// # Errors
676    ///  
677    /// May return any of the same errors as `std::fs::open`
678    pub fn create(&self) -> std::io::Result<()> {
679        if let Some(parent) = self.0.parent() {
680            if !parent.exists() {
681                fs::create_dir_all(parent)?;
682            }
683        }
684        fs::create_file(&self.0)
685    }
686
687    /// Removes a file from the filesystem.
688    ///
689    /// # Errors
690    ///
691    /// May return any of the same errors as `std::fs::remove_file`
692    pub fn remove(&self) -> std::io::Result<()> {
693        fs::remove_file(&self.0)
694    }
695
696    /// Given a path, queries the file system to get information about a file, directory, etc.
697    ///
698    /// # Errors
699    ///
700    /// May return any of the same errors as `std::fs::metadata`
701    pub fn fs_metadata(&self) -> std::io::Result<fs::Metadata> {
702        fs::metadata(&self.0)
703    }
704
705    /// Returns the final component of the path as a String.
706    /// See [`std::path::Path::file_name`] for more details.
707    ///
708    #[must_use]
709    #[allow(clippy::missing_panics_doc)]
710    pub fn file_name(&self) -> String {
711        self.0
712            .file_name()
713            .expect("validated in new")
714            .to_string_lossy()
715            .to_string()
716    }
717
718    /// Rename/move this file to a new path.
719    ///
720    /// Consumes the original instance and returns a new instance with the updated path on success.
721    /// On failure, returns both the error and the original instance for recovery.
722    /// Parent directories are created automatically if they don't exist.
723    ///
724    /// # Examples
725    ///
726    /// ```ignore
727    /// let file = GenericFile::new("old.txt");
728    /// match file.rename("new.txt") {
729    ///     Ok(renamed_file) => {
730    ///         // Use renamed_file
731    ///     }
732    ///     Err((error, original_file)) => {
733    ///         // Handle error, original_file is still available
734    ///     }
735    /// }
736    /// ```
737    ///
738    /// # Errors
739    ///  
740    /// May return any of the same errors as `std::fs::rename`
741    pub fn rename(
742        self,
743        new_path: impl AsRef<std::path::Path>,
744    ) -> Result<Self, (std::io::Error, Self)> {
745        let new_path = new_path.as_ref();
746
747        // Create parent directories if they don't exist
748        if let Some(parent) = new_path.parent() {
749            if !parent.exists() {
750                if let Err(e) = fs::create_dir_all(parent) {
751                    return Err((e, self));
752                }
753            }
754        }
755
756        // Attempt the rename
757        match fs::rename(&self.0, new_path) {
758            Ok(()) => Self::new(new_path).map_err(|e| (e, self)),
759            Err(e) => Err((e, self)),
760        }
761    }
762
763    /// Set file permissions to 0o600 (read/write for owner only).
764    ///
765    /// This method is only available on Unix systems.
766    ///
767    /// # Examples
768    ///
769    /// ```ignore
770    /// let file = GenericFile::new("secret.txt");
771    /// file.secure()?;
772    /// ```
773    ///
774    /// # Errors
775    ///  
776    /// May return any of the same errors as `std::fs::set_permissions`
777    #[cfg(unix)]
778    pub fn secure(&self) -> std::io::Result<()> {
779        use std::os::unix::fs::PermissionsExt;
780        let permissions = std::fs::Permissions::from_mode(0o600);
781        fs::set_permissions(&self.0, permissions)
782    }
783
784    /// Get the parent directory as `GenericDir`.
785    /// Files always have a parent directory.
786    #[must_use]
787    #[allow(clippy::missing_panics_doc)]
788    pub fn parent(&self) -> GenericDir {
789        let parent_path = self.0.parent().expect("Files must have a parent directory");
790        GenericDir::new(parent_path).expect("Parent path should be valid")
791    }
792}
793
794impl AsRef<std::path::Path> for GenericFile {
795    fn as_ref(&self) -> &std::path::Path {
796        &self.0
797    }
798}
799
800impl std::fmt::Display for GenericFile {
801    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802        write!(f, "{}", self.0.display())
803    }
804}
805
806impl std::fmt::Debug for GenericFile {
807    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808        write!(f, "GenericFile({})", self.0.display())
809    }
810}
811
812/// Generic directory type for type-erased directory operations.
813///
814/// This type can be used when you need to work with directories in a generic way,
815/// without knowing their specific type at compile time. It supports conversion
816/// to and from any typed directory struct.
817///
818/// # Examples
819///
820/// ```ignore
821/// use tree_type::{dir_type, GenericDir};
822///
823/// dir_type!(CacheDir);
824///
825/// let cache = CacheDir::new("/var/cache");
826/// let generic: GenericDir = cache.into();
827/// let cache_again = CacheDir::from_generic(generic);
828/// ```
829#[allow(unexpected_cfgs)]
830#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
831#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
832pub struct GenericDir(std::path::PathBuf);
833
834impl GenericDir {
835    /// Create a new wrapper for a directory
836    ///
837    /// # Errors
838    ///
839    /// If the directory is invalid. Invalid directory includes being empty.
840    pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
841        let path_buf = path.into();
842        // For directories, allow root paths and paths with filename components
843        // Only reject empty paths or invalid paths like ".."
844        if path_buf.as_os_str().is_empty() {
845            return Err(std::io::Error::from(ErrorKind::InvalidFilename));
846        }
847        Ok(Self(path_buf))
848    }
849
850    #[must_use]
851    pub fn as_path(&self) -> &std::path::Path {
852        &self.0
853    }
854
855    #[must_use]
856    pub fn exists(&self) -> bool {
857        self.0.exists()
858    }
859
860    /// Returns a clone of the `GenericDir`
861    #[must_use]
862    pub fn as_generic(&self) -> GenericDir {
863        self.clone()
864    }
865
866    /// Creates a new, empty directory at the provided path
867    ///
868    /// # Errors
869    ///  
870    /// May return any of the same errors as `std::fs::create_dir`
871    pub fn create(&self) -> std::io::Result<()> {
872        fs::create_dir(&self.0)
873    }
874
875    /// Recursively create a directory and all of its parent components if they are missing.
876    ///
877    /// # Errors
878    ///
879    /// May return any of the same errors as `std::fs::create_dir_all`
880    pub fn create_all(&self) -> std::io::Result<()> {
881        fs::create_dir_all(&self.0)
882    }
883
884    /// Removes an empty directory.
885    ///
886    /// # Errors
887    ///
888    /// May return any of the same errors as `std::fs::remove_all`
889    pub fn remove(&self) -> std::io::Result<()> {
890        fs::remove_dir(&self.0)
891    }
892
893    /// Removes a directory at this path, after removing all its contents. Use carefully!
894    ///
895    /// # Errors
896    ///
897    /// May return any of the same errors as `std::fs::remove_dir_all`
898    pub fn remove_all(&self) -> std::io::Result<()> {
899        fs::remove_dir_all(&self.0)
900    }
901
902    /// Returns an iterator over the entries within a directory.
903    ///
904    /// # Errors
905    ///
906    /// May return any of the same errors as `std::fs::read_dir`
907    #[cfg(feature = "enhanced-errors")]
908    pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<GenericPath>>> {
909        crate::deps::enhanced_errors::read_dir(&self.0)
910            .map(|read_dir| read_dir.map(|result| result.and_then(GenericPath::try_from)))
911    }
912
913    /// Returns an iterator over the entries within a directory.
914    ///
915    /// # Errors
916    ///
917    /// May return any of the same errors as `std::fs::read_dir`
918    #[cfg(not(feature = "enhanced-errors"))]
919    pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<GenericPath>>> {
920        std::fs::read_dir(&self.0)
921            .map(|read_dir| read_dir.map(|result| result.and_then(GenericPath::try_from)))
922    }
923
924    /// Given a path, queries the file system to get information about a file, directory, etc.
925    ///
926    /// # Errors
927    ///
928    /// May return any of the same errors as `std::fs::metadata`
929    pub fn fs_metadata(&self) -> std::io::Result<std::fs::Metadata> {
930        fs::metadata(&self.0)
931    }
932
933    #[cfg(feature = "walk")]
934    pub fn walk_dir(&self) -> crate::deps::walk::WalkDir {
935        crate::deps::walk::WalkDir::new(&self.0)
936    }
937
938    #[cfg(feature = "walk")]
939    pub fn walk(&self) -> impl Iterator<Item = std::io::Result<GenericPath>> {
940        crate::deps::walk::WalkDir::new(&self.0)
941            .into_iter()
942            .map(|r| r.map_err(|e| e.into()).and_then(GenericPath::try_from))
943    }
944
945    #[cfg(feature = "walk")]
946    /// Calculate total size in bytes of directory contents
947    pub fn size_in_bytes(&self) -> std::io::Result<u64> {
948        let mut total = 0u64;
949        for entry in crate::deps::walk::WalkDir::new(&self.0) {
950            let entry = entry?;
951            if entry.file_type().is_file() {
952                total += entry.metadata()?.len();
953            }
954        }
955        Ok(total)
956    }
957
958    #[cfg(feature = "walk")]
959    /// List directory contents with metadata
960    pub fn lsl(&self) -> std::io::Result<Vec<(std::path::PathBuf, std::fs::Metadata)>> {
961        let mut results = Vec::new();
962        for entry in crate::deps::walk::WalkDir::new(&self.0) {
963            let entry = entry?;
964            let metadata = entry.metadata()?;
965            results.push((entry.path().to_path_buf(), metadata));
966        }
967        Ok(results)
968    }
969
970    /// Returns the final component of the path as a String.
971    /// For root paths like "/", returns an empty string.
972    /// See [`std::path::Path::file_name`] for more details.
973    #[must_use]
974    pub fn file_name(&self) -> String {
975        self.0
976            .file_name()
977            .map(|name| name.to_string_lossy().to_string())
978            .unwrap_or_default()
979    }
980
981    /// Renames this directory to a new name, replacing the original item if
982    /// `to` already exists.
983    ///
984    /// Consumes the original instance and returns a new instance with the updated path on success.
985    /// On failure, returns both the error and the original instance for recovery.
986    /// Parent directories are created automatically if they don't exist.
987    ///
988    /// # Examples
989    ///
990    /// ```ignore
991    /// let dir = GenericDir::new("old_dir");
992    /// match dir.rename("new_dir") {
993    ///     Ok(renamed_dir) => {
994    ///         // Use renamed_dir
995    ///     }
996    ///     Err((error, original_dir)) => {
997    ///         // Handle error, original_dir is still available
998    ///     }
999    /// }
1000    /// ```
1001    ///
1002    /// # Errors
1003    ///  
1004    /// May return any of the same errors as `std::fs::rename`
1005    pub fn rename(
1006        self,
1007        new_path: impl AsRef<std::path::Path>,
1008    ) -> Result<Self, (std::io::Error, Self)> {
1009        let new_path = new_path.as_ref();
1010
1011        // Create parent directories if they don't exist
1012        if let Some(parent) = new_path.parent() {
1013            if !parent.exists() {
1014                if let Err(e) = std::fs::create_dir_all(parent) {
1015                    return Err((e, self));
1016                }
1017            }
1018        }
1019
1020        // Attempt the rename
1021        match fs::rename(&self.0, new_path) {
1022            Ok(()) => Self::new(new_path).map_err(|e| (e, self)),
1023            Err(e) => Err((e, self)),
1024        }
1025    }
1026
1027    /// Set directory permissions to 0o700 (read/write/execute for owner only).
1028    ///
1029    /// This method is only available on Unix systems.
1030    ///
1031    /// # Examples
1032    ///
1033    /// ```ignore
1034    /// let dir = GenericDir::new("secret_dir");
1035    /// dir.secure()?;
1036    /// ```
1037    ///
1038    /// # Errors
1039    ///  
1040    /// May return any of the same errors as `std::fs::set_permissions`
1041    #[cfg(unix)]
1042    pub fn secure(&self) -> std::io::Result<()> {
1043        use std::os::unix::fs::PermissionsExt;
1044        let permissions = std::fs::Permissions::from_mode(0o700);
1045        fs::set_permissions(&self.0, permissions)
1046    }
1047
1048    /// Get the parent directory as `GenericDir`.
1049    /// Returns None for directories at the root level.
1050    #[must_use]
1051    pub fn parent(&self) -> Option<GenericDir> {
1052        self.0
1053            .parent()
1054            .and_then(|parent_path| GenericDir::new(parent_path).ok())
1055    }
1056}
1057
1058impl AsRef<std::path::Path> for GenericDir {
1059    fn as_ref(&self) -> &std::path::Path {
1060        &self.0
1061    }
1062}
1063
1064impl std::fmt::Display for GenericDir {
1065    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1066        write!(f, "{}", self.0.display())
1067    }
1068}
1069
1070impl std::fmt::Debug for GenericDir {
1071    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1072        write!(f, "GenericDir({})", self.0.display())
1073    }
1074}
1075
1076/// Outcome of calling `create_default()` on a file type
1077#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1078pub enum CreateDefaultOutcome {
1079    /// File was created with default content
1080    Created,
1081    /// File already existed, no action taken
1082    AlreadyExists,
1083}
1084
1085/// Error type for `setup()` operations
1086#[derive(Debug)]
1087pub enum BuildError {
1088    /// Error creating a directory
1089    Directory(std::path::PathBuf, std::io::Error),
1090    /// Error creating a file with default content
1091    File(std::path::PathBuf, Box<dyn std::error::Error>),
1092}
1093
1094/// Controls whether validation/ensure operations recurse into child paths
1095#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1096pub enum Recursive {
1097    /// Validate/ensure only this level
1098    No,
1099    /// Validate/ensure entire tree
1100    Yes,
1101}
1102
1103/// Validation error for a specific path
1104#[derive(Debug, Clone, PartialEq, Eq)]
1105pub struct ValidationError {
1106    /// Path that failed validation
1107    pub path: std::path::PathBuf,
1108    /// Error message
1109    pub message: String,
1110}
1111
1112/// Validation warning for a specific path
1113#[derive(Debug, Clone, PartialEq, Eq)]
1114pub struct ValidationWarning {
1115    /// Path that generated warning
1116    pub path: std::path::PathBuf,
1117    /// Warning message
1118    pub message: String,
1119}
1120
1121/// Result of validation operation
1122#[derive(Debug, Clone, PartialEq, Eq)]
1123pub struct ValidationReport {
1124    /// Validation errors
1125    pub errors: Vec<ValidationError>,
1126    /// Validation warnings
1127    pub warnings: Vec<ValidationWarning>,
1128}
1129
1130impl ValidationReport {
1131    /// Create empty validation report
1132    #[must_use]
1133    pub fn new() -> Self {
1134        Self {
1135            errors: Vec::new(),
1136            warnings: Vec::new(),
1137        }
1138    }
1139
1140    /// Check if validation passed (no errors)
1141    #[must_use]
1142    pub fn is_ok(&self) -> bool {
1143        self.errors.is_empty()
1144    }
1145
1146    /// Merge another report into this one
1147    pub fn merge(&mut self, other: ValidationReport) {
1148        self.errors.extend(other.errors);
1149        self.warnings.extend(other.warnings);
1150    }
1151}
1152
1153impl Default for ValidationReport {
1154    fn default() -> Self {
1155        Self::new()
1156    }
1157}
1158
1159/// Result returned by validator functions
1160#[derive(Debug, Default, Clone, PartialEq, Eq)]
1161pub struct ValidatorResult {
1162    /// Validation errors
1163    pub errors: Vec<String>,
1164    /// Validation warnings
1165    pub warnings: Vec<String>,
1166}