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
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()` - [deprecated] Create directory (parent must exist)
444//! -  `setup()` - [deprecated] Create directory and all child directories/files recursively
445//! -  `validate()` - [deprecated] Validate tree structure without creating anything
446//! -  `ensure()` - 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//! # Parent Navigation
513//!
514//! The `parent()` method provides type-safe navigation to parent directories. Tree-type offers three different `parent()` method variants depending on the type you're working with:
515//!
516//! ## Method Variants Comparison
517//!
518//! | Type | Method Signature | Return Type | Behavior |
519//! |------|------------------|-------------|----------|
520//! | `GenericFile` | `parent(&self)` | `GenericDir` | Always succeeds - files must have parents |
521//! | `GenericDir` | `parent(&self)` | `Option<GenericDir>` | May fail for root directories |
522//! | Generated types | `parent(&self)` | Exact parent type | Type-safe, no Option needed |
523//! | Generated root types | `parent(&self)` | `Option<GenericDir` | May fail if Root type is root directory |
524//!
525//! ## `GenericFile` Parent Method
526//!
527//! Files always have a parent directory, so `GenericFile::parent()` returns `GenericDir` directly:
528//!
529//! ```
530//! use tree_type::GenericFile;
531//! use std::path::Path;
532//!
533//! # fn main() -> std::io::Result<()> {
534//! let file = GenericFile::new("/path/to/file.txt")?;
535//! let parent_dir = file.parent();  // Returns GenericDir
536//! assert_eq!(parent_dir.as_path(), Path::new("/path/to"));
537//! # Ok(())
538//! # }
539//! ```
540//!
541//! ## `GenericDir` Parent Method
542//!
543//! Directories may not have a parent (root directories), so `GenericDir::parent()` returns `Option<GenericDir>`:
544//!
545//! ```
546//! use tree_type::GenericDir;
547//!
548//! # fn main() -> std::io::Result<()> {
549//! let dir = GenericDir::new("/path/to/dir")?;
550//! if let Some(parent_dir) = dir.parent() {
551//!     println!("Parent: {parent_dir}");
552//! } else {
553//!     println!("This is a root directory");
554//! }
555//! # Ok(())
556//! # }
557//! ```
558//!
559//! ## Generated Type Parent Method
560//!
561//! Generated types from `tree_type!` macro provide type-safe parent navigation that returns the exact parent type:
562//!
563//! ```
564//! #![expect(deprecated)]
565//! use tree_type::tree_type;
566//! use tree_type::GenericDir;
567//!
568//! tree_type! {
569//!     ProjectRoot {
570//!         src/ as SrcDir {
571//!             main("main.rs") as MainFile
572//!         }
573//!     }
574//! }
575//!
576//! # fn main() -> std::io::Result<()> {
577//! let project = ProjectRoot::new("/project")?;
578//! let src = project.src();
579//! let main_file = src.main();
580//!
581//! // Type-safe parent navigation - no Option needed
582//! let main_parent: SrcDir = main_file.parent();
583//! let src_parent: ProjectRoot = src.parent();
584//! let project_parent: Option<GenericDir> = project.parent();
585//! # Ok(())
586//! # }
587//! ```
588//!
589//! ## Safety Notes
590//!
591//! - `GenericFile::parent()` may panic if the file path has no parent (extremely rare)
592//! - `GenericDir::parent()` returns `None` for root directories
593//! - Generated type `parent()` methods are guaranteed to return valid parent types
594//!
595//! # Contributing
596//!
597//! Contributions are welcome! Please feel free to submit a Pull Request
598//! at <https://codeberg.org/kemitix/tree-type/issues>
599//!
600
601pub mod deps;
602pub mod fs;
603
604use std::io::ErrorKind;
605
606// Re-export the proc macros for user convenience
607pub use tree_type_proc_macro::dir_type;
608pub use tree_type_proc_macro::file_type;
609pub use tree_type_proc_macro::tree_type;
610
611pub enum GenericPath {
612    File(GenericFile),
613    Dir(GenericDir),
614}
615
616impl TryFrom<std::fs::DirEntry> for GenericPath {
617    type Error = std::io::Error;
618
619    fn try_from(value: std::fs::DirEntry) -> Result<Self, Self::Error> {
620        let path: &std::path::Path = &value.path();
621        GenericPath::try_from(path)
622    }
623}
624
625#[cfg(feature = "walk")]
626impl TryFrom<crate::deps::walk::DirEntry> for GenericPath {
627    type Error = std::io::Error;
628
629    fn try_from(value: crate::deps::walk::DirEntry) -> Result<Self, Self::Error> {
630        GenericPath::try_from(value.path())
631    }
632}
633
634#[cfg(feature = "enhanced-errors")]
635impl TryFrom<crate::deps::enhanced_errors::DirEntry> for GenericPath {
636    type Error = std::io::Error; // because ::fs_err::Error is private
637
638    fn try_from(value: crate::deps::enhanced_errors::DirEntry) -> Result<Self, Self::Error> {
639        let path: &std::path::Path = &value.path();
640        GenericPath::try_from(path)
641    }
642}
643
644impl TryFrom<&std::path::Path> for GenericPath {
645    type Error = std::io::Error;
646
647    fn try_from(path: &std::path::Path) -> Result<Self, Self::Error> {
648        let path = resolve_path(path)?;
649        if path.is_file() {
650            Ok(GenericPath::File(GenericFile { path }))
651        } else if path.is_dir() {
652            Ok(GenericPath::Dir(GenericDir { path }))
653        } else {
654            Err(std::io::Error::other("unsupported path type"))
655        }
656    }
657}
658
659impl std::fmt::Display for GenericPath {
660    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
661        match self {
662            GenericPath::File(file) => write!(f, "{}", file.path.display()),
663            GenericPath::Dir(dir) => write!(f, "{}", dir.path.display()),
664        }
665    }
666}
667
668impl std::fmt::Debug for GenericPath {
669    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670        match self {
671            GenericPath::File(file) => write!(f, "GenericPath::File({})", file.path.display()),
672            GenericPath::Dir(dir) => write!(f, "GenericPath::Dir({})", dir.path.display()),
673        }
674    }
675}
676
677fn resolve_path(path: &std::path::Path) -> Result<std::path::PathBuf, std::io::Error> {
678    use std::collections::HashSet;
679    let mut p = path.to_path_buf();
680    let mut seen = HashSet::new();
681    seen.insert(p.clone());
682    while p.is_symlink() {
683        p = p.read_link()?;
684        if seen.contains(&p) {
685            return Err(std::io::Error::other("symlink loop"));
686        }
687    }
688    Ok(p)
689}
690
691// Generate GenericFile using the proc macro
692#[allow(unexpected_cfgs)]
693#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
694#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
695pub struct GenericFile {
696    path: std::path::PathBuf,
697}
698
699impl GenericFile {
700    /// Create a new wrapper for a file
701    ///
702    /// # Errors
703    ///
704    /// If the file name is invalid. Invalid file name inclued empty file name.
705    pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
706        let path_buf = path.into();
707        let file_name = path_buf.file_name();
708        if file_name.is_none() {
709            return Err(std::io::Error::from(ErrorKind::InvalidFilename));
710        }
711        Ok(Self { path: path_buf })
712    }
713
714    #[must_use]
715    pub fn as_path(&self) -> &std::path::Path {
716        &self.path
717    }
718
719    #[must_use]
720    pub fn exists(&self) -> bool {
721        self.path.exists()
722    }
723
724    #[must_use]
725    pub fn as_generic(&self) -> GenericFile {
726        self.clone()
727    }
728
729    /// Reads the entire contents of a file into a bytes vector.
730    ///
731    /// # Errors
732    ///
733    /// May return any of the same errors as `std::fs::read`
734    pub fn read(&self) -> std::io::Result<Vec<u8>> {
735        fs::read(&self.path)
736    }
737
738    /// Reads the entire contents of a file into a string.
739    ///
740    /// # Errors
741    ///
742    /// May return any of the same errors as `std::fs::read_to_string`
743    pub fn read_to_string(&self) -> std::io::Result<String> {
744        fs::read_to_string(&self.path)
745    }
746
747    /// Writes a slice as the entire contents of a file.
748    ///
749    /// # Errors
750    ///
751    /// May return any of the same errors as `std::fs::write`
752    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
753        if let Some(parent) = self.path.parent() {
754            if !parent.exists() {
755                fs::create_dir_all(parent)?;
756            }
757        }
758        fs::write(&self.path, contents)
759    }
760
761    /// Create an empty file if it doesn't already exist
762    ///
763    /// # Errors
764    ///  
765    /// May return any of the same errors as `std::fs::open`
766    pub fn create(&self) -> std::io::Result<()> {
767        if let Some(parent) = self.path.parent() {
768            if !parent.exists() {
769                fs::create_dir_all(parent)?;
770            }
771        }
772        fs::create_file(&self.path)
773    }
774
775    /// Removes a file from the filesystem.
776    ///
777    /// # Errors
778    ///
779    /// May return any of the same errors as `std::fs::remove_file`
780    pub fn remove(&self) -> std::io::Result<()> {
781        fs::remove_file(&self.path)
782    }
783
784    /// Given a path, queries the file system to get information about a file, directory, etc.
785    ///
786    /// # Errors
787    ///
788    /// May return any of the same errors as `std::fs::metadata`
789    pub fn fs_metadata(&self) -> std::io::Result<fs::Metadata> {
790        fs::metadata(&self.path)
791    }
792
793    /// Returns the final component of the path as a String.
794    /// See [`std::path::Path::file_name`] for more details.
795    ///
796    #[must_use]
797    #[allow(clippy::missing_panics_doc)]
798    pub fn file_name(&self) -> String {
799        self.path
800            .file_name()
801            .expect("validated in new")
802            .to_string_lossy()
803            .to_string()
804    }
805
806    /// Rename/move this file to a new path.
807    ///
808    /// Consumes the original instance and returns a new instance with the updated path on success.
809    /// On failure, returns both the error and the original instance for recovery.
810    /// Parent directories are created automatically if they don't exist.
811    ///
812    /// # Examples
813    ///
814    /// ```
815    /// use tree_type::GenericFile;
816    ///
817    /// # fn main() -> std::io::Result<()> {
818    /// let file = GenericFile::new("old.txt")?;
819    /// match file.rename("new.txt") {
820    ///     Ok(renamed_file) => {
821    ///         // Use renamed_file
822    ///     }
823    ///     Err((error, original_file)) => {
824    ///         // Handle error, original_file is still available
825    ///     }
826    /// }
827    /// # Ok(())
828    /// # }
829    /// ```
830    ///
831    /// # Errors
832    ///  
833    /// May return any of the same errors as `std::fs::rename`
834    pub fn rename(
835        self,
836        new_path: impl AsRef<std::path::Path>,
837    ) -> Result<Self, (std::io::Error, Self)> {
838        let new_path = new_path.as_ref();
839
840        // Create parent directories if they don't exist
841        if let Some(parent) = new_path.parent() {
842            if !parent.exists() {
843                if let Err(e) = fs::create_dir_all(parent) {
844                    return Err((e, self));
845                }
846            }
847        }
848
849        // Attempt the rename
850        match fs::rename(&self.path, new_path) {
851            Ok(()) => Self::new(new_path).map_err(|e| (e, self)),
852            Err(e) => Err((e, self)),
853        }
854    }
855
856    /// Set file permissions to 0o600 (read/write for owner only).
857    ///
858    /// This method is only available on Unix systems.
859    ///
860    /// # Examples
861    ///
862    /// ```
863    /// use tree_type::GenericFile;
864    /// # use tempfile::TempDir;
865    ///
866    /// # fn main() -> std::io::Result<()> {
867    /// # let temp_dir = TempDir::new()?;
868    /// # let secret_file = temp_dir.path().join("secret.txt");
869    /// let file = GenericFile::new(secret_file)?;
870    /// file.write("content");
871    /// file.secure()?;
872    /// # Ok(())
873    /// # }
874    /// ```
875    ///
876    /// # Errors
877    ///  
878    /// May return any of the same errors as `std::fs::set_permissions`
879    #[cfg(unix)]
880    pub fn secure(&self) -> std::io::Result<()> {
881        use std::os::unix::fs::PermissionsExt;
882        let permissions = std::fs::Permissions::from_mode(0o600);
883        fs::set_permissions(&self.path, permissions)
884    }
885
886    /// Get the parent directory as `GenericDir`.
887    /// Files always have a parent directory.
888    #[must_use]
889    #[allow(clippy::missing_panics_doc)]
890    pub fn parent(&self) -> GenericDir {
891        let parent_path = self
892            .path
893            .parent()
894            .expect("Files must have a parent directory");
895        GenericDir::new(parent_path).expect("Parent path should be valid")
896    }
897}
898
899impl AsRef<std::path::Path> for GenericFile {
900    fn as_ref(&self) -> &std::path::Path {
901        &self.path
902    }
903}
904
905impl std::fmt::Display for GenericFile {
906    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907        write!(f, "{}", self.path.display())
908    }
909}
910
911impl std::fmt::Debug for GenericFile {
912    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913        write!(f, "GenericFile({})", self.path.display())
914    }
915}
916
917/// Generic directory type for type-erased directory operations.
918///
919/// This type can be used when you need to work with directories in a generic way,
920/// without knowing their specific type at compile time. It supports conversion
921/// to and from any typed directory struct.
922///
923/// # Examples
924///
925/// ```
926/// use tree_type::{dir_type, GenericDir};
927///
928/// dir_type!(CacheDir);
929///
930/// # fn main() -> std::io::Result<()> {
931/// let cache = CacheDir::new("/var/cache")?;
932/// let generic: GenericDir = cache.into();
933/// let cache_again = CacheDir::from_generic(generic);
934/// # Ok(())
935/// # }
936/// ```
937#[allow(unexpected_cfgs)]
938#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
939#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
940pub struct GenericDir {
941    path: std::path::PathBuf,
942}
943
944impl GenericDir {
945    /// Create a new wrapper for a directory
946    ///
947    /// # Errors
948    ///
949    /// If the directory is invalid. Invalid directory includes being empty.
950    pub fn new(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
951        let path_buf = path.into();
952        // For directories, allow root paths and paths with filename components
953        // Only reject empty paths or invalid paths like ".."
954        if path_buf.as_os_str().is_empty() {
955            return Err(std::io::Error::from(ErrorKind::InvalidFilename));
956        }
957        Ok(Self { path: path_buf })
958    }
959
960    #[must_use]
961    pub fn as_path(&self) -> &std::path::Path {
962        &self.path
963    }
964
965    #[must_use]
966    pub fn exists(&self) -> bool {
967        self.path.exists()
968    }
969
970    /// Returns a clone of the `GenericDir`
971    #[must_use]
972    pub fn as_generic(&self) -> GenericDir {
973        self.clone()
974    }
975
976    /// Creates a new, empty directory at the provided path
977    ///
978    /// # Errors
979    ///  
980    /// May return any of the same errors as `std::fs::create_dir`
981    pub fn create(&self) -> std::io::Result<()> {
982        fs::create_dir(&self.path)
983    }
984
985    /// Recursively create a directory and all of its parent components if they are missing.
986    ///
987    /// # Errors
988    ///
989    /// May return any of the same errors as `std::fs::create_dir_all`
990    pub fn create_all(&self) -> std::io::Result<()> {
991        fs::create_dir_all(&self.path)
992    }
993
994    /// Removes an empty directory.
995    ///
996    /// # Errors
997    ///
998    /// May return any of the same errors as `std::fs::remove_all`
999    pub fn remove(&self) -> std::io::Result<()> {
1000        fs::remove_dir(&self.path)
1001    }
1002
1003    /// Removes a directory at this path, after removing all its contents. Use carefully!
1004    ///
1005    /// # Errors
1006    ///
1007    /// May return any of the same errors as `std::fs::remove_dir_all`
1008    pub fn remove_all(&self) -> std::io::Result<()> {
1009        fs::remove_dir_all(&self.path)
1010    }
1011
1012    /// Returns an iterator over the entries within a directory.
1013    ///
1014    /// # Errors
1015    ///
1016    /// May return any of the same errors as `std::fs::read_dir`
1017    #[cfg(feature = "enhanced-errors")]
1018    pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<GenericPath>>> {
1019        crate::deps::enhanced_errors::read_dir(&self.path)
1020            .map(|read_dir| read_dir.map(|result| result.and_then(GenericPath::try_from)))
1021    }
1022
1023    /// Returns an iterator over the entries within a directory.
1024    ///
1025    /// # Errors
1026    ///
1027    /// May return any of the same errors as `std::fs::read_dir`
1028    #[cfg(not(feature = "enhanced-errors"))]
1029    pub fn read_dir(&self) -> std::io::Result<impl Iterator<Item = std::io::Result<GenericPath>>> {
1030        std::fs::read_dir(&self.path)
1031            .map(|read_dir| read_dir.map(|result| result.and_then(GenericPath::try_from)))
1032    }
1033
1034    /// Given a path, queries the file system to get information about a file, directory, etc.
1035    ///
1036    /// # Errors
1037    ///
1038    /// May return any of the same errors as `std::fs::metadata`
1039    pub fn fs_metadata(&self) -> std::io::Result<std::fs::Metadata> {
1040        fs::metadata(&self.path)
1041    }
1042
1043    #[cfg(feature = "walk")]
1044    #[must_use]
1045    pub fn walk_dir(&self) -> crate::deps::walk::WalkDir {
1046        crate::deps::walk::WalkDir::new(&self.path)
1047    }
1048
1049    #[cfg(feature = "walk")]
1050    pub fn walk(&self) -> impl Iterator<Item = std::io::Result<GenericPath>> {
1051        crate::deps::walk::WalkDir::new(&self.path)
1052            .into_iter()
1053            .map(|r| {
1054                r.map_err(std::convert::Into::into)
1055                    .and_then(GenericPath::try_from)
1056            })
1057    }
1058
1059    #[cfg(feature = "walk")]
1060    /// Calculate total size in bytes of directory contents
1061    ///
1062    /// # Errors
1063    ///
1064    /// Returns an error if there are issues accessing files or directories during traversal.
1065    pub fn size_in_bytes(&self) -> std::io::Result<u64> {
1066        let mut total = 0u64;
1067        for entry in crate::deps::walk::WalkDir::new(&self.path) {
1068            let entry = entry?;
1069            if entry.file_type().is_file() {
1070                total += entry.metadata()?.len();
1071            }
1072        }
1073        Ok(total)
1074    }
1075
1076    #[cfg(feature = "walk")]
1077    /// List directory contents with metadata
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns an error if there are issues accessing files or directories during traversal.
1082    pub fn lsl(&self) -> std::io::Result<Vec<(std::path::PathBuf, std::fs::Metadata)>> {
1083        let mut results = Vec::new();
1084        for entry in crate::deps::walk::WalkDir::new(&self.path) {
1085            let entry = entry?;
1086            let metadata = entry.metadata()?;
1087            results.push((entry.path().to_path_buf(), metadata));
1088        }
1089        Ok(results)
1090    }
1091
1092    /// Returns the final component of the path as a String.
1093    /// For root paths like "/", returns an empty string.
1094    /// See [`std::path::Path::file_name`] for more details.
1095    #[must_use]
1096    pub fn file_name(&self) -> String {
1097        self.path
1098            .file_name()
1099            .map(|name| name.to_string_lossy().to_string())
1100            .unwrap_or_default()
1101    }
1102
1103    /// Renames this directory to a new name, replacing the original item if
1104    /// `to` already exists.
1105    ///
1106    /// Consumes the original instance and returns a new instance with the updated path on success.
1107    /// On failure, returns both the error and the original instance for recovery.
1108    /// Parent directories are created automatically if they don't exist.
1109    ///
1110    /// # Examples
1111    ///
1112    /// ```
1113    /// use tree_type::GenericDir;
1114    ///
1115    /// # fn main() -> std::io::Result<()> {
1116    /// let dir = GenericDir::new("old_dir")?;
1117    /// match dir.rename("new_dir") {
1118    ///     Ok(renamed_dir) => {
1119    ///         // Use renamed_dir
1120    ///     }
1121    ///     Err((error, original_dir)) => {
1122    ///         // Handle error, original_dir is still available
1123    ///     }
1124    /// }
1125    /// # Ok(())
1126    /// # }
1127    /// ```
1128    ///
1129    /// # Errors
1130    ///  
1131    /// May return any of the same errors as `std::fs::rename`
1132    pub fn rename(
1133        self,
1134        new_path: impl AsRef<std::path::Path>,
1135    ) -> Result<Self, (std::io::Error, Self)> {
1136        let new_path = new_path.as_ref();
1137
1138        // Create parent directories if they don't exist
1139        if let Some(parent) = new_path.parent() {
1140            if !parent.exists() {
1141                if let Err(e) = std::fs::create_dir_all(parent) {
1142                    return Err((e, self));
1143                }
1144            }
1145        }
1146
1147        // Attempt the rename
1148        match fs::rename(&self.path, new_path) {
1149            Ok(()) => Self::new(new_path).map_err(|e| (e, self)),
1150            Err(e) => Err((e, self)),
1151        }
1152    }
1153
1154    /// Set directory permissions to 0o700 (read/write/execute for owner only).
1155    ///
1156    /// This method is only available on Unix systems.
1157    ///
1158    /// # Examples
1159    ///
1160    /// ```
1161    /// use tree_type::GenericDir;
1162    /// # use tempfile::TempDir;
1163    ///
1164    /// # fn main() -> std::io::Result<()> {
1165    /// # let temp_dir = TempDir::new()?;
1166    /// # let secret_dir = temp_dir.path().join("secret");
1167    /// let dir = GenericDir::new(secret_dir)?;
1168    /// dir.create()?;
1169    /// dir.secure()?;
1170    /// # Ok(())
1171    /// # }
1172    /// ```
1173    ///
1174    /// # Errors
1175    ///  
1176    /// May return any of the same errors as `std::fs::set_permissions`
1177    #[cfg(unix)]
1178    pub fn secure(&self) -> std::io::Result<()> {
1179        use std::os::unix::fs::PermissionsExt;
1180        let permissions = std::fs::Permissions::from_mode(0o700);
1181        fs::set_permissions(&self.path, permissions)
1182    }
1183
1184    /// Get the parent directory as `GenericDir`.
1185    /// Returns None for directories at the root level.
1186    #[must_use]
1187    pub fn parent(&self) -> Option<GenericDir> {
1188        self.path
1189            .parent()
1190            .and_then(|parent_path| GenericDir::new(parent_path).ok())
1191    }
1192}
1193
1194impl AsRef<std::path::Path> for GenericDir {
1195    fn as_ref(&self) -> &std::path::Path {
1196        &self.path
1197    }
1198}
1199
1200impl std::fmt::Display for GenericDir {
1201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1202        write!(f, "{}", self.path.display())
1203    }
1204}
1205
1206impl std::fmt::Debug for GenericDir {
1207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1208        write!(f, "GenericDir({})", self.path.display())
1209    }
1210}
1211
1212/// Outcome of calling `create_default()` on a file type
1213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1214pub enum CreateDefaultOutcome {
1215    /// File was created with default content
1216    Created,
1217    /// File already existed, no action taken
1218    AlreadyExists,
1219}
1220
1221/// Error type for `setup()` operations
1222#[derive(Debug)]
1223pub enum BuildError {
1224    /// Error creating a directory
1225    Directory(std::path::PathBuf, std::io::Error),
1226    /// Error creating a file with default content
1227    File(std::path::PathBuf, Box<dyn std::error::Error>),
1228}
1229
1230/// Controls whether validation/ensure operations recurse into child paths
1231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1232pub enum Recursive {
1233    /// Validate/ensure only this level
1234    No,
1235    /// Validate/ensure entire tree
1236    Yes,
1237}
1238
1239/// Validation error for a specific path
1240#[derive(Debug, Clone, PartialEq, Eq)]
1241pub struct ValidationError {
1242    /// Path that failed validation
1243    pub path: std::path::PathBuf,
1244    /// Error message
1245    pub message: String,
1246}
1247
1248/// Validation warning for a specific path
1249#[derive(Debug, Clone, PartialEq, Eq)]
1250pub struct ValidationWarning {
1251    /// Path that generated warning
1252    pub path: std::path::PathBuf,
1253    /// Warning message
1254    pub message: String,
1255}
1256
1257/// Result of validation operation
1258#[derive(Debug, Clone, PartialEq, Eq)]
1259pub struct ValidationReport {
1260    /// Validation errors
1261    pub errors: Vec<ValidationError>,
1262    /// Validation warnings
1263    pub warnings: Vec<ValidationWarning>,
1264}
1265
1266impl ValidationReport {
1267    /// Create empty validation report
1268    #[must_use]
1269    pub fn new() -> Self {
1270        Self {
1271            errors: Vec::new(),
1272            warnings: Vec::new(),
1273        }
1274    }
1275
1276    /// Check if validation passed (no errors)
1277    #[must_use]
1278    pub fn is_ok(&self) -> bool {
1279        self.errors.is_empty()
1280    }
1281
1282    /// Merge another report into this one
1283    pub fn merge(&mut self, other: ValidationReport) {
1284        self.errors.extend(other.errors);
1285        self.warnings.extend(other.warnings);
1286    }
1287}
1288
1289impl Default for ValidationReport {
1290    fn default() -> Self {
1291        Self::new()
1292    }
1293}
1294
1295/// Result returned by validator functions
1296#[derive(Debug, Default, Clone, PartialEq, Eq)]
1297pub struct ValidatorResult {
1298    /// Validation errors
1299    pub errors: Vec<String>,
1300    /// Validation warnings
1301    pub warnings: Vec<String>,
1302}