Nestum
If your Rust app already has real command, event, or error trees, nested enums are often the honest model.
They keep family boundaries in the type system, but the call sites get noisy fast.
Before:
state.publish;
return Err;
match self
After:
state.publish;
return Err;
match self
nestum keeps the same nested-enum model, keeps the same compile-time invariant, and removes most of the tuple-wrapping tax.
When Nestum Is Worth It
Use nestum when all of these are true:
- the outer enum is already a real envelope over command, event, message, or error families
- that family boundary carries real correctness information
- you construct and match those envelopes often enough that wrapper syntax is now the main pain
- you want to keep normal derive-heavy Rust enums instead of flattening the model
Strong fits usually look like:
- error envelopes
- command trees
- event and message trees
Do Not Use Nestum If...
- you would invent a hierarchy just to get prettier syntax
- the outer enum is a one-off wrapper and helper functions already hide the noise
- flattening the model would actually be clearer for the domain
- the nesting path depends on
#[cfg],#[cfg_attr],include!(),#[path = "..."], or macro-generated local enums - the nested inner enum lives in an external crate
Flagship Use Case
The strongest example in this repo is nestum-examples/src/todo_api/app.rs.
It keeps three separate nested trees at the application boundary:
Commandfor the work the API can performEventfor the domain events it emitsErrorfor validation and persistence failures
That boundary looks like this:
That shape stays honest all the way through the app:
- route handlers build nested commands
- the app service matches nested command families
- response mapping matches nested error families
- event publishing emits nested event families
nestum is most valuable when the same tree shows up across several of those call sites, not just one constructor.
Quick Start
use ;
let event: Enum = Created;
nested!
Migration Guide
The todo_api example is a good migration model because it uses nested enums at a real boundary instead of in a toy demo.
Start with the honest nested enums you already have:
Then change the call sites that currently pay the wrapper tax.
Before:
let command = Todos;
state.publish;
match self
After:
let command = nested! ;
state.publish;
nested!
The data model did not change. The envelope shape did not change. The syntax got closer to the tree you were already modeling.
Cookbooks
thiserror: Nested Error Envelopes
nestum works well when an outer error envelope preserves the error family boundary and thiserror handles display, source chaining, and #[from].
use ;
use Error;
let err: Enum = InvalidTitle.into;
let ok = nested! ;
assert!;
The test suite also covers transitive #[from] through nested error trees and rejects ambiguous conversions.
clap: Command Trees
The ops_cli example keeps the command hierarchy honest and lets dispatch read like the CLI tree.
nested!
This is the kind of command surface where nestum tends to pay for itself quickly.
serde: Preserve the Envelope Shape
nestum does not flatten nested enums before serialization. serde still sees the real wrapped structure.
A nested value like Event::Document::Renamed { title: "Spec".to_string() } serializes as:
That makes nestum a good fit when the nested structure itself matters on the wire.
Axum: Response Mapping and Route Commands
The todo_api example shows both sides:
- route handlers build nested commands from request payloads
IntoResponsematches nested error branches directly
let command = nested! ;
let = nested! ;
This is a strong pattern when handlers, services, and error mapping all share the same command or error tree.
Mental Model
#[nestum]turns an enum name into a namespace for nested-path constructors.nested! { ... }rewrites nested constructors and nested patterns where Rust syntax needs help.#[nestum_scope]rewrites a whole function, impl, method, or inline module body when localnested!wrappers would get noisy.Outer::Enum<T>is the underlying enum type when you need it in a type position.
Event::Document::Created is not a flattened replacement for the underlying enum. It is syntax over the same nested model.
Real-World Examples
The nestum-examples workspace crate includes:
todo_api: Axum + SQLite-backed todo API with nested commands, events, and errorsops_cli: Clap command tree with nested dispatch
Run them with:
No Type-Safety Trade
nestum is syntax and namespace machinery over real nested enums.
- it keeps the same compile-time family boundaries
- it does not replace those boundaries with strings or runtime tags
- it keeps derive-heavy enums compatible with the rest of the ecosystem
Authority Surface
Within its supported observation point, nestum treats parsed crate-local source plus proc-macro source locations as authoritative for nested-path expansion.
That means:
- source locations for proc-macro expansion must be available
- every module and enum on the nesting path must be directly present in parsed crate-local source
#[cfg]and#[cfg_attr]on modules, enums, variants, or enum fields are rejected for nesting resolution#[path = "..."],include!(), and macro-generated local enums are outside that authority surface
Unsupported cases are rejected where nestum can detect them. When source-location context is unavailable, nestum now errors instead of guessing.
API
#[nestum]
Marks an enum so nested enum-wrapping variants can be constructed through path-shaped syntax.
nested! { ... }
Rewrites nested constructors and nested patterns into ordinary Rust enum syntax.
Use it for:
matchif letwhile letlet-elsematches!assert!,debug_assert!,assert_eq!,assert_ne!, and debug variants- named-field nested construction
#[nestum_scope]
Rewrites nested constructors and nested patterns across a wider body.
Use it on:
- functions
- impl methods
- impl blocks
- inline modules
#[nestum(external = "path::to::Enum")]
Marks a variant as wrapping a nested enum defined in another crate-local module file.
nestum_match! { match value { ... } }
Match-only compatibility macro.
Prefer nested! unless you specifically want a match-only entry point.
Limitations
nestuminspects parsed crate-local source plus proc-macro source locations, not macro-expanded or type-checked items- external crates are not supported as nested inner enums because proc macros cannot reliably inspect dependency sources
macro_rules!-generated local enums are not supported as nested inner enums#[cfg],#[cfg_attr],#[path = "..."], andinclude!()are unsupported on the nesting path- most other outer macro token trees are still opaque to
#[nestum_scope] - qself or associated paths are rejected for nested field detection
Coding Agents
If you use coding agents, see docs/agents/. It includes copyable instruction templates, an opportunity-signals guide, an audit playbook, and prompts for audits, greenfield design, review, and targeted refactors.
License
MIT