tree-type
Type-safe path navigation macros for Rust projects with fixed directory structures.
Quick Example
Basic usage example showing type-safe navigation and setup.
use tree_type;
// Define your directory structure
tree_type!
// Each path gets its own type (which cna be overridden)
let project = new?;
let src = project.src; // ProjectRootSrc
let readme = project.readme; // ProjectRootReadme
process_source?;
// process_source(&readme)?; // would be a compilation error
// 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:
| feature | description | dependencies |
|---|---|---|
serde |
Adds Serialize/Deserialize derives to all path types | serde |
enhanced-errors |
Enhanced error messages for filesystem operations | fs-err, path_facts |
walk |
Directory traversal methods | walkdir |
pattern-validation |
Regex pattern validation for dynamic ID blocks | regex |
codegen-v2 |
Unlimited nesting depth for dynamic IDs (experimental) | none |
# With all features
[]
= { = "0.1.0", = ["serde", "enhanced-errors", "walk", "pattern-validation"] }
Unlimited Nesting (codegen-v2)
The default code generation limits dynamic ID nesting to 3 levels. Enable codegen-v2 for unlimited nesting depth:
[]
= { = "0.1.0", = ["codegen-v2"] }
This allows structures like:
tree_type!
Custom type requirements: When using custom types for dynamic IDs, they must implement:
Display,PartialOrd,Ord,Hash- With
serdefeature: alsoSerialize,Deserialize
;
Transparent Serialization (codegen-v2)
The #[transparent] attribute enables transparent serialization and memory representation for generated types. This feature requires codegen-v2 and works with the serde feature.
What it does:
- Generates
#[repr(transparent)]for zero-cost FFI compatibility - Generates
#[serde(transparent)]when serde feature enabled - Types serialize as their inner path value, not as wrapper objects
Example:
use tree_type;
tree_type!
let root = new?;
root.sync?;
let config = root.config;
// Serializes as path string: "/app/config.toml"
// NOT as object: {"path": "/app/config.toml"}
let json = to_string?;
assert!;
Requirements:
- Only works with
codegen-v2feature - Types must be single-field wrappers (compiler enforced)
- Serde serialization requires
serdefeature
Usage Examples
Basic Tree Structure
Specify the type that represent the root of your tree, it will be a directory. Then within
{} specify the identifiers of the files and directories that are children of the
root. Directories as identified by having a trailing / after their identifier, otherwise
they are files.
use tree_type;
tree_type!
let project = new?;
let src = project.src; // ProjectRootSrc
let readme = project.readme; // ProjectRootReadme
assert_eq!;
assert_eq!;
Custom Filenames
By default the filename will be the same as the identifier, (as long is it is valid for the filesystem).
To specify an alternative filename, e.g. one where the filename isn't a valid Rust
identifier, specify the filename as identifier/("file-name"). Note that the directory
indicator (/) comes after the identifier, not the directory name.
use tree_type;
tree_type!
let home = new?;
let ssh = home.ssh; // UserHomeSsh (maps to .ssh)
let key = ssh.ecdsa_public; // UserHomeSshEcdsaPublic (maps to id_ecdsa.pub)
assert_eq!;
assert_eq!;
Custom Type Names
tree_type will generate type names for each file and directory by appending the capitalised
identifier to the parent type, unless you override this with as.
use tree_type;
tree_type!
let project = new?;
let src: ProjectRootSrc = project.src;
let main: ProjectRootSrcMain = src.main;
let readme: ReadmeFile = project.readme;
}
Default File Content
Create files with default content if they don't exist:
use ;
tree_type!
let project = new?;
let changelog = project.changelog;
let readme = project.readme;
let config = project.config;
changelog.write?;
assert!; // an existing file
assert!; // don't exist yet
assert!;
match project.setup
assert!; // created and set to default content
assert_eq!;
assert!; // created and function sets the content
assert!;
assert!; // existing file is left unchanged
assert_eq!;
The function f in #[default(f)]:
- Takes
&FileTypeas parameter (self-aware, can access own path) - Returns
Result<String, E>where E can be any error type - Allows network requests, file I/O, parsing, etc.
- Errors are propagated to the caller
The setup() method:
- Creates all directories in the tree
- Creates all files with a
#[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)
Dynamic ID
Dynamic ID support allows you to define parameterized paths in your directory structure where the actual directory/file names are determined at runtime using ID parameters.
use tree_type;
use ValidatorResult;
tree_type!
let root = new?;
let user_dir: UserDir = root.users.user_id;
let _result = user_dir.setup;
assert_eq!;
assert!; // required + default
assert!; // not required and/or no default
assert_eq!;
let log_file = root.logs.log_name;
assert_eq!;
assert!; // we need to create this ourselves
// FIXME: can't validate a filename until the file exists
log_file.write?;
// validation fails because `foo.log` doesn't start with `log-`
// FIXME: `validate()` should return a `Result<T, E>` rather then a ValidationReport
let report = root.logs.validate;
assert!;
assert_eq!;
assert!;
File Type Macro
The file_type macro provides for when you only need to work with a single file rather
than a directory structure. You would use it instead of tree_type when you only need to
manage one file, not a directory tree, or when you need to treat several files in a directory
tree in a more generic way.
The tree_type macro uses the file_type macro to represent any files defined in it.
use file_type;
file_type!;
let config_file = new?;
config_file.write?;
assert!;
let config = config_file.read_to_string?;
assert_eq!;
File Operations
File types support:
display()- Get Display object for formatting pathsread_to_string()- Read file as stringread()- Read file as byteswrite()- Write content to filecreate_default()- Create file with default content if it doesn't existexists()- Check if file existsremove()- Delete filefs_metadata()- Get file metadatasecure()(Unix only) - Set permissions to 0o600
use tree_type;
tree_type!
let project = new?;
let readme = project.readme;
// Write content to file
readme.write?;
assert!;
// Read content back
let content = readme.read_to_string?;
assert_eq!;
Directory Type Macro
The dir_type macro provides for when you only need to work with a single directory rather
than a nested directory structure. You would use it instead of tree_type when you only need
to manage one directory, not a directory tree, or when you need to treat several directories
in a more generic way.
The tree_type macro uses the dir_type macro to represent the directories defined in it.
use dir_type;
dir_type!;
let config_dir = new?;
config_dir.create_all?;
handle_config?;
Directory Operations
Directory types support:
display()- Get Display object for formatting pathscreate_all()- Create directory and parentscreate()- [deprecated] Create directory (parent must exist)setup()- [deprecated] Create directory and all child directories/files recursivelyvalidate()- [deprecated] Validate tree structure without creating anythingensure()- 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 recursively
Symbolic links
Create (soft) symbolic links to other files or directories in the tree.
This feature is only available on unix-like environments (i.e. #[cfg(unix)]).
use tree_type;
tree_type!
let app = new?;
let _result = app.setup;
assert!;
assert!;
// /config/active.toml -> /config/prod.toml
assert_eq!;
// /data/config -> /config/active.toml -> /config/prod.toml
assert_eq!;
Symlink targets must exist, so the target should have a #[required] attribute for
directories, or #[default...] attribute for files.
Parent Navigation
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:
Method Variants Comparison
| Type | Method Signature | Return Type | Behavior |
|---|---|---|---|
GenericFile |
parent(&self) |
GenericDir |
Always succeeds - files must have parents |
GenericDir |
parent(&self) |
Option<GenericDir> |
May fail for root directories |
| Generated types | parent(&self) |
Exact parent type | Type-safe, no Option needed |
| Generated root types | parent(&self) |
Option<GenericDir |
May fail if Root type is root directory |
GenericFile Parent Method
Files always have a parent directory, so GenericFile::parent() returns GenericDir directly:
use GenericFile;
use Path;
let file = new?;
let parent_dir = file.parent; // Returns GenericDir
assert_eq!;
GenericDir Parent Method
Directories may not have a parent (root directories), so GenericDir::parent() returns Option<GenericDir>:
use GenericDir;
let dir = new?;
if let Some = dir.parent else
Generated Type Parent Method
Generated types from tree_type! macro provide type-safe parent navigation that returns the exact parent type:
use tree_type;
use GenericDir;
tree_type!
let project = new?;
let src = project.src;
let main_file = src.main;
// Type-safe parent navigation - no Option needed
let main_parent: SrcDir = main_file.parent;
let src_parent: ProjectRoot = src.parent;
let project_parent: = project.parent;
Safety Notes
GenericFile::parent()may panic if the file path has no parent (extremely rare)GenericDir::parent()returnsNonefor root directories- Generated type
parent()methods are guaranteed to return valid parent types
Contributing
Contributions are welcome! Please feel free to submit a Pull Request at https://codeberg.org/kemitix/tree-type/issues
License: MIT