tree-type
Type-safe path navigation macros for Rust projects with fixed directory structures.
Quick Example
use tree_type;
// Define your directory structure
tree_type!
// Each path gets its own type
// Type-safe navigation
let project = new;
let src = project.src; // ProjectRootSrc
let readme = project.readme; // ProjectRootReadme
process_source?;
// Setup entire structure
project.setup?; // Creates src/, target/, and all files
Overview
tree-type provides macros for creating type-safe filesystem path types:
tree_type!- Define a tree of path types with automatic navigation methodsdir_type!- Convenience wrapper for simple directory types (no children)file_type!- Convenience macro for simple file types (single file with operations)
Features
- Type Safety: Each path in your directory structure gets its own type
- Navigation Methods: Automatic generation of navigation methods
- Custom Names: Support for custom type names and filenames
- Dynamic IDs: ID-based navigation for dynamic directory structures
- Rich Operations: Built-in filesystem operations (create, read, write, remove, etc.)
Installation
Add to your Cargo.toml:
[]
= "0.1.0"
Features
All features are opt-in to minimize dependencies:
-
serde(opt-in): Adds Serialize/Deserialize derives to all path types -
enhanced-errors(opt-in): Enhanced error messages for filesystem operations- fs-err: Better error messages for all filesystem operations
- path_facts: Detailed path information in error context
-
walk(opt-in): Directory traversal methods- walkdir: Recursive directory iteration
- Adds
walk_dir(),walk(),size_in_bytes(), andlsl()methods to directory types
-
pattern-validation(opt-in): Regex pattern validation for dynamic ID blocks- regex: Regular expression matching
- once_cell: Lazy static pattern compilation
- Enables
#[pattern(regex)]attribute on dynamic ID blocks
# With serde support
[]
= { = "0.1.0", = ["serde"] }
# With enhanced error messages
[]
= { = "0.1.0", = ["enhanced-errors"] }
# With directory walking
[]
= { = "0.1.0", = ["walk"] }
# With pattern validation
[]
= { = "0.1.0", = ["pattern-validation"] }
# With all features
[]
= { = "0.1.0", = ["serde", "enhanced-errors", "walk", "pattern-validation"] }
Default: Minimal dependencies (only paste)
Usage
Basic Tree Structure
use tree_type;
tree_type!
let project = new;
let src = project.src; // ProjectRootSrc
let readme = project.readme; // ProjectRootReadme
assert_eq!;
assert_eq!;
Custom Filenames
tree_type!
let home = new;
let ssh = home.ssh; // UserHomeSsh (maps to .ssh)
let key = ssh.ecdsa_public; // UserHomeSshEcdsaPublic (maps to id_ecdsa.pub)
Custom Type Names
tree_type!
let refs = new;
let heads = refs.heads; // HeadsDir (not RepoGitRefsDirHeads)
Dynamic IDs
tree_type!
let issues = new;
let issue = issues.id;
let file = issue.file;
// Convenience methods
let file = issues.id_file;
let metadata = issues.id_metadata;
Dynamic ID Attributes
Dynamic ID blocks support several attributes for validation and default instances:
Content Validation
Attributes inside dynamic blocks work like regular directories - they validate the contents of each matching directory:
tree_type!
// Every directory in tasks/ must contain metadata.toml
// and may optionally contain notes.md
Pattern Validation
Validate directory names against regex patterns (requires pattern-validation feature):
tree_type!
// Only directories matching "task-1", "task-42", etc. are valid
// Validation fails for "invalid-name" or "task-abc"
Enable the feature in Cargo.toml:
[]
= { = "0.1.0", = ["pattern-validation"] }
Validation Functions
Use custom validation functions for complex naming rules:
tree_type!
// Validation uses your custom function
// No additional features required
Default Instances
Create a default directory instance during setup:
tree_type!
let tasks = new;
tasks.setup?; // Creates tasks/example/ directory
// Combine with other attributes
tree_type!
Default File Content
Create files with default content if they don't exist:
use ;
tree_type!
let project = new;
let config = project.config;
match config.create_default
The default function:
- Takes
&FileTypeas parameter (self-aware, can access path) - Returns
Result<String, E>whereEcan be any error type - Allows network requests, file I/O, parsing, etc.
- Errors are propagated to the caller
Setting Up with Default Files
The setup() method automatically creates files with default content:
tree_type!
let project = new;
// Creates src/ directory AND readme + config files with default content
match project.setup
The setup() method:
- Creates all directories in the tree
- Calls
create_default()on all files with#[default = function]attribute - Collects all errors and continues processing (doesn't fail fast)
- Returns
Result<(), Vec<BuildError>>with all errors if any occurred - Skips files that already exist (won't overwrite)
This provides a convenient one-call setup for entire project structures including both directories and files with default content.
Symbolic Links
Create symbolic links to other files in the tree:
use tree_type;
tree_type!
let config = new;
// Create the symlink (target file created automatically due to #[default])
config.setup.unwrap; // Creates active.toml -> prod.toml
// Now active.toml points to production.toml
assert!;
Cross-Directory Symlinks:
Use path syntax for symlinks across directories:
tree_type!
Key Points:
- Symlinks are created when
setup()is called on the parent directory - Sibling symlinks: Use identifier
#[symlink = target] - Cross-directory symlinks: Use path string
#[symlink = /path/to/target] - Platform support: Unix-only (Linux, macOS, BSD)
Common Use Cases:
tree_type!
Limitations:
- No individual
ensure()method on symlinks (use parent'ssetup()) - Compile-time validation requires symlink targets to have
#[default = ...]or#[required]attributes - No circular reference detection
- Path resolution is relative to parent directory
Troubleshooting Symlinks:
If you encounter symlink compilation errors, here are common solutions:
-
"symlink target 'X' may not exist at runtime":
- Add
#[default("content")]to the target file to ensure it exists during setup() - Or add
#[required]if the file should be created manually before setup()
- Add
-
"symlink target 'X' not found in tree structure":
- Ensure the target identifier exists in the same directory for sibling symlinks
- Use absolute path syntax
/path/to/targetfor cross-directory symlinks
-
Symlinks not created during setup():
- Verify the target file exists or has default content
- Check that setup() is called on the parent directory containing the symlink
Example fixes:
tree_type!
See docs/symlink-validation-limitations.md for detailed information.
Validation Attributes
Mark paths as required or optional for validation:
tree_type!
let project = new;
// Validate checks required paths exist
let report = project.validate;
if !report.is_ok
// Ensure validates and creates missing required paths
project.ensure?;
Attributes work with all syntax forms:
tree_type!
Default behavior:
- Paths without attributes are
#[optional]by default - Children of required parents are still optional unless marked
#[required]
When to use explicit #[optional]:
- For documentation when mixing with
#[required]siblings - To make intent clear in code reviews
- When optionality is a key design decision
tree_type!
Standalone Types
Create individual file or directory types:
use ;
file_type!;
dir_type!;
let config = new;
if config.exists
let cache = new;
cache.setup?;
File Operations
File types support:
display()- Get Display object for formatting pathsread_to_string()- Read file as string (enhanced errors withenhanced-errorsfeature)read()- Read file as byteswrite()- Write content to filecreate_default()- Create file with default content if it doesn't exist (requires#[default = function]attribute)exists()- Check if file existsremove()- Delete filefs_metadata()- Get file metadatasecure()(Unix only) - Set permissions to 0o600
Note: With the enhanced-errors feature, read_to_string() includes detailed path information in error messages.
Directory Operations
Directory types support:
display()- Get Display object for formatting pathscreate_all()- Create directory and parentscreate()- Create directory (parent must exist)setup()- Create directory and all child directories/files recursivelyvalidate(recursive)- Validate tree structure without creating anythingensure(recursive)- Validate and create missing required pathsexists()- Check if directory existsread_dir()- List directory contentsremove()- Remove empty directoryremove_all()- Remove directory recursivelyfs_metadata()- Get directory metadata
With walk feature enabled:
walk_dir()- Walk directory tree (returns iterator)walk()- Walk with callbacks for dirs/filessize_in_bytes()- Calculate total size recursivelylsl()- List directory contents (debug output)
Setting Up Directory Trees
The setup() method creates the entire directory structure defined in your tree:
tree_type!
let project = new;
project.setup?; // Creates src/, target/, config/, config/local/, config/prod/
For dynamic ID directories, setup() discovers existing ID directories and builds their children:
tree_type!
let issues = new;
// Manually create some issue directories
issues.id.create?;
issues.id.create?;
// Setup discovers existing issue dirs and creates their children
issues.setup?; // Creates attachments/ and comments/ for each existing issue
The setup() method:
- Creates the directory itself plus all descendants recursively
- Calls
create_default()on all files with#[default = function]attribute - For dynamic IDs, discovers existing directories and calls
setup()on each - Collects all errors and continues processing (doesn't fail fast)
- Returns
Result<(), Vec<BuildError>>with all errors if any occurred - Is idempotent - safe to call multiple times
Validation API
Validate directory structures without creating them:
use ;
let project = new;
// Validate entire tree
let report = project.validate;
if !report.is_ok
// Validate and create missing required paths
match project.ensure
The validation API provides:
validate(recursive)- Read-only validation, returnsValidationReportensure(recursive)- Validates then creates missing required pathsRecursive::Yes- Validate/ensure entire treeRecursive::No- Validate/ensure only this level
Examples
See the tests for comprehensive examples including:
- Nested directory structures
- Multiple levels of dynamic IDs
- Mixed file and directory types
- Custom filenames and type names
Changelog
Version 0.2.0 (2025-12-05)
Bug Fixes:
- Fixed required files with default content functions not being created by
setup()orensure() - Files with
#[required, default = fn]now correctly created in all scenarios:- Root level files
- Nested directories
- Dynamic ID blocks
- Custom type files
- Fixed
ensure()method to create directories before validating, preventing false validation failures - Added recursive
ensure()processing for directories with children
Implementation:
- Added ~180 lines of pattern matching rules across 5 macro helper sections
- Pattern normalization for
#[required, default = ...]and#[optional, default = ...]combinations - Validation logic updated to skip errors for files with defaults (since
ensure()will create them)
License
This crate was extracted from the fireforge project.