nestum 0.3.1

Proc-macro for nested enum paths like Enum1::Variant1::VariantA
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
<div align="center">
  <img alt="nestum logo" src="https://raw.githubusercontent.com/eboody/nestum/main/nestum.png" width="360">
  <p>Nestum keeps the honest nested-enum model and removes the wrapping noise.</p>
  <p>
    <a href="https://github.com/eboody/nestum/actions/workflows/ci.yml"><img src="https://github.com/eboody/nestum/actions/workflows/ci.yml/badge.svg?branch=main&event=push" alt="build status" /></a>
    <a href="https://crates.io/crates/nestum"><img src="https://img.shields.io/crates/v/nestum.svg?logo=rust" alt="crates.io" /></a>
    <a href="https://docs.rs/nestum"><img src="https://docs.rs/nestum/badge.svg" alt="docs.rs" /></a>
  </p>
</div>

# 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:

```rust
state.publish(Event::Todos(todo::Event::Created(todo.clone())));
return Err(Error::Todos(todo::Error::NotFound(id)));

match self {
    Error::Validation(ValidationError::EmptyTitle) => { /* ... */ }
    Error::Todos(todo::Error::NotFound(id)) => { /* ... */ }
    Error::Todos(todo::Error::Database(message)) => { /* ... */ }
}
```

After:

```rust
state.publish(Event::Todos::Created(todo.clone()));
return Err(Error::Todos::NotFound(id));

match self {
    Error::Validation::EmptyTitle => { /* ... */ }
    Error::Todos::NotFound(id) => { /* ... */ }
    Error::Todos::Database(message) => { /* ... */ }
}
```

`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`](./nestum-examples/src/todo_api/app.rs).

It keeps three separate nested trees at the application boundary:

- `Command` for the work the API can perform
- `Event` for the domain events it emits
- `Error` for validation and persistence failures

That boundary looks like this:

```rust
#[nestum]
#[derive(Debug, Clone)]
pub enum Command {
    Health(super::health::Command),
    Todos(super::todo::Command),
}

#[nestum]
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(tag = "stream", content = "event", rename_all = "snake_case")]
pub enum Event {
    Todos(super::todo::Event),
}

#[nestum]
#[derive(Debug)]
pub enum Error {
    Validation(super::ValidationError),
    Todos(super::todo::Error),
}
```

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

```bash
cargo add nestum
```

```rust
use nestum::{nestum, nested};

#[nestum]
enum DocumentEvent {
    Created,
    Deleted,
}

#[nestum]
enum Event {
    Document(DocumentEvent),
}

let event: Event::Enum = Event::Document::Created;

nested! {
    match event {
        Event::Document::Created => {}
        Event::Document::Deleted => {}
    }
}
```

## 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:

```rust
#[nestum]
pub enum Error {
    Validation(super::ValidationError),
    Todos(super::todo::Error),
}
```

Then change the call sites that currently pay the wrapper tax.

Before:

```rust
let command = app::Command::Todos(todo::Command::Create {
    title: payload.title.try_into()?,
});

state.publish(Event::Todos(todo::Event::Created(todo.clone())));

match self {
    Error::Validation(ValidationError::EmptyTitle) => (
        http::StatusCode::UNPROCESSABLE_ENTITY,
        ErrorBody {
            error: "validation",
            detail: "title must not be blank".to_string(),
        },
    ),
    Error::Todos(todo::Error::NotFound(id)) => (
        http::StatusCode::NOT_FOUND,
        ErrorBody {
            error: "todo_not_found",
            detail: format!("todo {id} does not exist"),
        },
    ),
    Error::Todos(todo::Error::Database(message)) => (
        http::StatusCode::INTERNAL_SERVER_ERROR,
        ErrorBody {
            error: "database",
            detail: message,
        },
    ),
}
```

After:

```rust
let command = nested! {
    app::Command::Todos::Create {
        title: payload.title.try_into()?,
    }
};

state.publish(Event::Todos::Created(todo.clone()));

nested! {
    match self {
        Error::Validation::EmptyTitle => (
            http::StatusCode::UNPROCESSABLE_ENTITY,
            ErrorBody {
                error: "validation",
                detail: "title must not be blank".to_string(),
            },
        ),
        Error::Todos::NotFound(id) => (
            http::StatusCode::NOT_FOUND,
            ErrorBody {
                error: "todo_not_found",
                detail: format!("todo {id} does not exist"),
            },
        ),
        Error::Todos::Database(message) => (
            http::StatusCode::INTERNAL_SERVER_ERROR,
            ErrorBody {
                error: "database",
                detail: message,
            },
        ),
    }
}
```

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]`.

```rust
use nestum::{nestum, nested};
use thiserror::Error;

#[nestum]
#[derive(Debug, Error)]
pub enum DocumentError {
    #[error("document not found")]
    NotFound,
    #[error("invalid title: {0}")]
    InvalidTitle(String),
}

#[nestum]
#[derive(Debug, Error)]
pub enum ApiError {
    #[error(transparent)]
    Document(#[from] DocumentError),
    #[error("transport error")]
    Transport,
}

let err: ApiError::Enum = DocumentError::InvalidTitle("draft".to_string()).into();

let ok = nested! {
    matches!(err, ApiError::Document::InvalidTitle(title) if title == "draft")
};
assert!(ok);
```

The test suite also covers transitive `#[from]` through nested error trees and rejects ambiguous conversions.

### `clap`: Command Trees

The [`ops_cli`](./nestum-examples/src/ops_cli.rs) example keeps the command hierarchy honest and lets dispatch read like the CLI tree.

```rust
#[nestum]
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
    #[command(subcommand)]
    Users(command::User),
    #[command(subcommand)]
    Billing(command::Billing),
}

nested! {
    match self {
        Command::Users::Create(args) => format!("create-user:{}", args.email),
        Command::Users::Suspend { user_id } => format!("suspend-user:{user_id}"),
        Command::Billing::Charge(args) => {
            format!("charge-invoice:{}:{}c", args.invoice_id, args.cents)
        }
        Command::Billing::Refund { invoice_id } => format!("refund-invoice:{invoice_id}"),
    }
}
```

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.

```rust
#[nestum]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocumentEvent {
    Created { id: u64 },
    Renamed { title: String },
}

#[nestum]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Event {
    Document(DocumentEvent),
    Health,
}
```

A nested value like `Event::Document::Renamed { title: "Spec".to_string() }` serializes as:

```json
{
  "Document": {
    "Renamed": {
      "title": "Spec"
    }
  }
}
```

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
- `IntoResponse` matches nested error branches directly

```rust
let command = nested! {
    app::Command::Todos::Rename {
        id,
        title: payload.title.try_into()?,
    }
};

let (status, body) = nested! {
    match self {
        Error::Validation::EmptyTitle => (
            http::StatusCode::UNPROCESSABLE_ENTITY,
            ErrorBody {
                error: "validation",
                detail: "title must not be blank".to_string(),
            },
        ),
        Error::Todos::NotFound(id) => (
            http::StatusCode::NOT_FOUND,
            ErrorBody {
                error: "todo_not_found",
                detail: format!("todo {id} does not exist"),
            },
        ),
        Error::Todos::Database(message) => (
            http::StatusCode::INTERNAL_SERVER_ERROR,
            ErrorBody {
                error: "database",
                detail: message,
            },
        ),
    }
};
```

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 local `nested!` 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`](./nestum-examples) workspace crate includes:

- `todo_api`: Axum + SQLite-backed todo API with nested commands, events, and errors
- `ops_cli`: Clap command tree with nested dispatch

Run them with:

```bash
cargo run -p nestum-examples --bin todo_api
cargo run -p nestum-examples --bin ops_cli -- users create dev@example.com
```

## 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:

- `match`
- `if let`
- `while let`
- `let-else`
- `matches!`
- `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

- `nestum` inspects 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 = "..."]`, and `include!()` 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/`](./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