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}