Nestum
When nested enums are the honest model, they buy you real compile-time invariants.
If Event::Document wraps DocumentEvent, then the type system already rules out the wrong family. You cannot accidentally construct a document envelope around a user event. The problem is not correctness. The problem is that the accurate model gets noisy fast:
Construct:
Created
instead of:
Document
and match the same way with nested!.
use ;
let inner: Enum = Created;
let event: Enum = Created;
nested!
let _ = inner;
nestum keeps the nested-enum design, keeps the invariant, and removes most of the wrapping noise.
Correctness First
Use nestum when nested enums are already the right way to model the domain.
Event::Documentalways contains aDocumentEvent.Event::Useralways contains aUserEvent.- cross-family mistakes stay impossible at compile time.
nestum does not flatten the model, erase the family boundary, or swap compile-time guarantees for runtime checks. It generates nicer constructor and pattern surfaces over the same nested-enum structure.
Mental Model
#[nestum]on an enum turns the 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 so you do not need as many localnested!wrappers.- If you need the concrete enum type itself, use
Outer::Enum<T>.
That namespace tradeoff is what makes Event::Document::Created possible in ordinary Rust syntax.
What This Path Means
DocumentEvent::Createdconstructs aDocumentEvent::Enum.Event::Document::Createdconstructs anEvent::Enum.Event::Documentis a namespace branch, not a completedEventvalue.
Good fits for nestum look like:
Event::Document::CreatedCommand::User::CreateMessage::Billing::Paid
Weak fits usually look like:
- a one-off wrapper where helper functions already hide the wrapping noise
- a hierarchy you would have to invent just to get prettier syntax
- names like
Document::Event::Createdthat read like inner type namespaces instead of outer envelope values
nestum is strongest when the outer enum is already a real envelope over event, command, or message families.
That usually means:
- the nested enums are already the most accurate model of the problem
- the family boundary is carrying real correctness information
- the pain is mostly constructor and match noise
Quick Start
- Add
#[nestum]to each enum in the hierarchy. - Construct wrapped values with nested paths like
Event::Document::Created. - Use
nested! { ... }for focused rewrites, or put#[nestum_scope]on the enclosing function, impl, method, or inline module to rewrite a wider scope at once.
Real-World Showcases
The nestum-examples workspace crate shows the macro against real libraries instead of toy enums.
todo_api: Axum + in-memory SQLite + broadcast events. This keeps command families, domain errors, and emitted events as separate nested enum trees instead of flattening them for convenience.ops_cli: Clap subcommands with nested dispatch. This keeps the command tree honest at the type level while still letting the dispatch code read like the tree it models.
The example crate also shows the API-shape side of that approach: todo_api is split into app, health, and todo modules, and ops_cli exposes inner command families under command::{User, Billing} instead of repeating long top-level type prefixes.
Run them with:
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.
Examples
Basic Nesting
let _ = Created;
let _ = Archived;
This is the core use case: keep the real family boundary in the type system, but stop paying for it with tuple-wrapping boilerplate.
Named-Field Constructors
Use nested! when the nested leaf is a named-field variant:
use ;
let value: Enum = nested! ;
Scope-Level Rewriting
Use #[nestum_scope] when a function or impl body has several nested constructors or patterns:
use ;
Cross-Module Nesting
Use #[nestum(external = "...")] when the inner enum lives in another module file:
let _ = A;
Ecosystem Compatibility
nestum still works with common derive-heavy Rust crates. The test suite covers:
serderound trips for wrapped outer enumsthiserrorderives with transparent outer error envelopes- transitive
#[from]conversions from leaf errors into nested outer error envelopes - common assertion macros, including
assert!(matches!(...))
Core Rules
- Only enums are supported.
- Both the outer enum and the nested inner enum need
#[nestum]. - Use
nested!for focused rewrites, or#[nestum_scope]for function-, impl-, method-, or inline-module-level rewrites.
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 weaken the invariant that an outer branch contains the right inner enum family.
- The tradeoffs are mostly in syntax, naming, and tooling, not in static correctness.
Authority Surface
Within its supported observation point, nestum treats the parsed crate-local enum/module tree as authoritative for nested-path expansion.
- 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.
Advanced Notes
- In type positions, use
Outer::Enum<T>for the enum type itself. - Put
#[nestum]before#[derive(...)]so derive macros see the rewritten enum shape. serde,thiserror, and common assertion macros are covered by tests.- When each nested error wrapper opts into
#[from], leaf errors can use?all the way into the outer envelope. - Generic outer enums use functions for unit constructors, so
Outer::Other()orOuter::Wrap::Ready()may be function calls instead of constants. - Plain local inner enum paths,
self::...,super::..., and qualified crate-local inner enum paths are supported, including generic arguments. - Cross-module nesting is explicit with
#[nestum(external = "crate::path::Enum")]. - For nested variants, the raw root constructor path like
Outer::Wrap(inner)is no longer part of the public surface; useOuter::Enum::Wrap(inner)if you need the explicit underlying constructor. #[nestum_scope]rewrites normal Rust AST inside the annotated item body and also handlesmatches!,assert!,debug_assert!,assert_eq!,assert_ne!, and their debug variants.
Limitations
- Most other outer macro token trees are still opaque to
#[nestum_scope]. - qself or associated paths are rejected for nested field detection.
nestuminspects parsed crate-local source plus proc-macro source locations, not macro-expanded or type-checked items.- Build environments must preserve proc-macro source locations; when that context is unavailable,
nestumnow errors instead of guessing. #[cfg]and#[cfg_attr]on modules, enums, variants, or enum fields are unsupported for nesting resolution.macro_rules!-generated local enums are not supported as nested inner enums.#[path = "..."],include!(), and complexcfgmodule layouts may not resolve.- External crates are not supported because proc macros cannot reliably inspect dependency sources.
API
#[nestum]
Marks an enum so nested enum-wrapping variants can be constructed through path-shaped syntax.
use nestum;
let _ = A;
nested! { ... }
Rewrites nested constructors and nested patterns into ordinary Rust enum syntax.
Use it for match, if let, while let, let-else, matches!, common assertion macros, and named-field nested construction.
use ;
let value = B;
let ok = nested! ;
#[nestum_scope]
Rewrites nested constructors and nested patterns across a wider body.
Use it on functions, impl methods, impl blocks, or inline modules when local nested! wrappers would get noisy.
use ;
#[nestum(external = "path::to::Enum")]
Marks a variant as wrapping a nested enum defined in another module file.
use nestum;
nestum_match! { match value { ... } }
Match-only compatibility macro. Prefer nested! unless you specifically want a match-only entry point.
License
MIT