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}