pub enum Node {
File { name: String, size: u64 },
Dir { name: String, children: Vec<Node> },
}
impl Node {
pub fn total_size(&self) -> u64 {
match self {
Node::File { size, .. } => *size,
Node::Dir { children, .. } => {
children.iter().map(|child| child.total_size()).sum()
}
}
}
pub fn count_files(&self) -> usize {
match self {
Node::File { .. } => 1,
Node::Dir { children, .. } => {
children.iter().map(|child| child.count_files()).sum()
}
}
}
pub fn name(&self) -> &str {
match self {
Node::File { name, .. } => name,
Node::Dir { name, .. } => name,
}
}
}
pub fn file(name: &str, size: u64) -> Node {
Node::File { name: name.to_string(), size }
}
pub fn dir(name: &str, children: Vec<Node>) -> Node {
Node::Dir { name: name.to_string(), children }
}
mod fs_tree;
use fs_tree::{dir, file};
fn main() {
let root = dir(
"project",
vec![
file("README.md", 2_048),
dir(
"src",
vec![
file("main.rs", 1_200),
file("fs_tree.rs", 3_400),
dir("bin", vec![file("cli.rs", 900)]),
],
),
dir("assets", vec![]),
],
);
println!("root: {}", root.name());
println!("total size: {} bytes", root.total_size());
println!("file count: {}", root.count_files());
}
#[cfg(test)]
mod tests {
use crate::fs_tree::{dir, file};
#[test]
fn single_file_is_its_own_size() {
let node = file("a.txt", 42);
assert_eq!(node.total_size(), 42);
assert_eq!(node.count_files(), 1);
}
#[test]
fn empty_dir_has_zero_size() {
let node = dir("empty", vec![]);
assert_eq!(node.total_size(), 0);
assert_eq!(node.count_files(), 0);
}
#[test]
fn nested_dirs_sum_recursively() {
let tree = dir(
"root",
vec![
file("x", 10),
dir("sub", vec![file("y", 5), file("z", 7)]),
],
);
assert_eq!(tree.total_size(), 22);
assert_eq!(tree.count_files(), 3);
}
}
This snippet models a directory tree as a recursive data structure and walks it to compute aggregate statistics like total byte size. The fs_tree module defines the core type as an enum Node with two variants: File carries a name and a byte count, while Dir carries a name and an owned Vec<Node> of children. Modeling the tree this way makes the mutual recursion explicit — a directory contains nodes, and any of those nodes may itself be a directory — which mirrors the real shape of a filesystem far better than a flat list would.
A subtle but important detail is that Dir stores Vec<Node> by value rather than references. Because Rust enforces ownership, each parent owns its children outright, so the whole tree can be dropped in one move and no lifetimes leak into the type signature. If children were shared or cyclic, Rc/RefCell would be required instead, but a directory tree is naturally acyclic, so plain ownership is the right, cheapest tool.
The method total_size in fs_tree module is the heart of the traversal. It uses a match on self: the Node::File arm returns the stored size directly (the base case), and the Node::Dir arm folds over its children with iter().map(...).sum(), recursing into each child. This is depth-first recursion expressed declaratively — every node is visited exactly once, and results bubble up as the stack unwinds. The companion count_files shows the same shape for a different accumulator.
Builder helpers file and dir keep construction readable and avoid repetitive struct literals when assembling nested trees. In main.rs, dir and file compose a small tree, then total_size and count_files are called on the root. Because recursion depth follows directory nesting, extremely deep trees could risk stack overflow; a production tool handling untrusted input would switch to an explicit work-stack, but for typical trees the recursive form is clearer and correct. The tests module pins the base case, an empty directory, and a nested case, guarding against regressions in the fold logic.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Called {}", stringify!($func_name));
}
};
Declarative macros (macro_rules!) for code generation
Share this code
Here's the card — post it anywhere.