standout-dispatch 7.5.0

Command dispatch and routing for clap-based CLIs
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
# Execution Model

`standout-dispatch` manages a strict linear pipeline from CLI input to rendered output. This explicitly separated flow ensures that logic (handlers) remains decoupled from presentation (renderers) and side-effects (hooks).

---

## The Pipeline

```text
Clap Parsing → Pre-dispatch → Handler → Post-dispatch → Renderer → Post-output → Piping → Output
```

Each stage has a clear responsibility:

**Clap Parsing**: Your `clap::Command` definition is parsed normally. `standout-dispatch` doesn't replace clap—it works with the resulting `ArgMatches`.

**Pre-dispatch Hook**: Runs before the handler. Can abort execution (e.g., auth checks).

**Handler**: Your logic function executes. It receives `ArgMatches` and `CommandContext`, returning a `HandlerResult<T>`—either data to render, a silent marker, or binary content. For simpler handlers, use the `#[handler]` macro to write pure functions that return `Result<T, E>` directly (see [Handler Contract](handler-contract.md)).

**Post-dispatch Hook**: Runs after the handler, before rendering. Can transform data.

**Renderer**: Your render function receives the data and produces output (string or binary).

**Post-output Hook**: Runs after rendering. Can transform the final output string.

**Piping**: Optionally sends output to external commands (jq, tee, clipboard). Implemented as specialized post-output hooks. See [Output Piping](../../standout-pipe/docs/topics/piping.md).

**Output**: The result is returned or written to stdout.

---

## Command Paths

A command path is a vector of strings representing the subcommand chain:

```bash
myapp db migrate --steps 5
```

The command path is `["db", "migrate"]`.

### Extracting Command Paths

```rust
use standout_dispatch::{extract_command_path, path_to_string, get_deepest_matches};

let matches = cmd.get_matches();

// Get the full path
let path = extract_command_path(&matches);  // ["db", "migrate"]

// Convert to dot notation
let path_str = path_to_string(&path);  // "db.migrate"

// Get ArgMatches for the deepest command
let deep = get_deepest_matches(&matches);  // ArgMatches for "migrate"
```

### Command Path Utilities

| Function | Purpose |
|----------|---------|
| `extract_command_path` | Get subcommand chain as `Vec<String>` |
| `path_to_string` | Convert path to dot notation (`"db.migrate"`) |
| `string_to_path` | Convert dot notation to path |
| `get_deepest_matches` | Get `ArgMatches` for deepest subcommand |
| `has_subcommand` | Check if any subcommand was invoked |

---

## State Injection

Handlers access state through `CommandContext`, which provides two mechanisms:

- **`app_state`**: Shared, immutable state configured at build time (database, config)
- **`extensions`**: Per-request, mutable state injected by hooks

```rust
fn handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<T> {
    // App state: shared resources
    let db = ctx.app_state.get_required::<Database>()?;

    // Extensions: per-request state
    let scope = ctx.extensions.get_required::<UserScope>()?;
    // ...
}
```

> For full details on state management, see [App State and Extensions]app-state.md.

---

## The Hooks System

Hooks are functions that run at specific points in the pipeline. They let you intercept, validate, or transform without touching handler logic—keeping concerns separated.

### Three Phases

**Pre-dispatch**: Runs before the handler. Can abort execution or inject per-request state.

Use for: authentication checks, input validation, logging start time, **injecting per-request state** via `extensions`.

Pre-dispatch hooks receive `&mut CommandContext`, allowing them to inject state via `ctx.extensions` that handlers can retrieve. They also have read access to `ctx.app_state` for shared resources:

```rust
use standout_dispatch::{Hooks, HookError};

// Per-request state types (injected by hooks)
struct UserSession { user_id: u64 }

Hooks::new()
    .pre_dispatch(|matches, ctx| {
        // Read from app_state (shared)
        let db = ctx.app_state.get_required::<Database>()?;

        // Validate and set up per-request state
        let token = std::env::var("API_TOKEN")
            .map_err(|_| HookError::pre_dispatch("API_TOKEN required"))?;

        let user_id = db.validate_token(&token)?;

        // Inject into extensions (per-request)
        ctx.extensions.insert(UserSession { user_id });
        Ok(())
    })
```

Handlers then use both app_state and extensions:

```rust
fn list_handler(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
    // App state: shared across all requests
    let db = ctx.app_state.get_required::<Database>()?;

    // Extensions: per-request state from hooks
    let session = ctx.extensions.get_required::<UserSession>()?;

    let items = db.fetch_items(session.user_id)?;
    Ok(Output::Render(items))
}
```

See the [Handler Contract](handler-contract.md#extensions) for full `Extensions` API documentation, and [App State](app-state.md) for details on the two-state model.

**Post-dispatch**: Runs after the handler, before rendering. Can transform data.

Use for: adding timestamps, filtering sensitive fields, data enrichment. The hook receives handler output as `serde_json::Value`, allowing generic transformations regardless of the handler's output type.

```rust
Hooks::new().post_dispatch(|_matches, _ctx, mut data| {
    if let Some(obj) = data.as_object_mut() {
        obj.insert("generated_at".into(), json!(Utc::now().to_rfc3339()));
    }
    Ok(data)
})
```

**Post-output**: Runs after rendering. Can transform the final string.

Use for: adding headers/footers, logging, metrics. The hook receives `RenderedOutput`—an enum of `Text(String)`, `Binary(Vec<u8>, String)`, or `Silent`.

```rust
use standout_dispatch::RenderedOutput;

Hooks::new().post_output(|_matches, _ctx, output| {
    match output {
        RenderedOutput::Text(s) => {
            Ok(RenderedOutput::Text(format!("{}\n-- Generated by MyApp", s)))
        }
        other => Ok(other),
    }
})
```

### Hook Chaining

Multiple hooks per phase are supported. Pre-dispatch hooks run sequentially—first error aborts. Post-dispatch and post-output hooks *chain*: each receives the output of the previous, enabling composable transformations.

```rust
Hooks::new()
    .post_dispatch(add_metadata)      // Runs first
    .post_dispatch(filter_sensitive)  // Receives add_metadata's output
```

Order matters: `filter_sensitive` sees the metadata that `add_metadata` inserted.

### Output Piping

Piping sends rendered output to external shell commands. It's implemented as specialized post-output hooks with three modes:

```rust
use standout::cli::App;

let app = App::builder()
    .commands(|g| {
        g.command_with("export", handlers::export, |cfg| {
            cfg.template("export.jinja")
               // Filter through jq (capture mode)
               .pipe_through("jq '.items'")
        })
        .command_with("copy", handlers::copy, |cfg| {
            cfg.template("copy.jinja")
               // Send to clipboard (consume mode)
               .pipe_to_clipboard()
        })
        .command_with("debug", handlers::debug, |cfg| {
            cfg.template("debug.jinja")
               // Log to file while displaying (passthrough mode)
               .pipe_to("tee /tmp/debug.log")
        })
    })
    .build()?;
```

| Mode | Method | Behavior |
|------|--------|----------|
| Passthrough | `pipe_to()` | Run command, return original output |
| Capture | `pipe_through()` | Return command's stdout as new output |
| Consume | `pipe_to_clipboard()` | Send to clipboard, return empty |

Pipes can be chained and combined with other post-output hooks. See [Output Piping](../../standout-pipe/docs/topics/piping.md) for full documentation.

### Error Handling

When a hook returns `Err(HookError)`:

- Execution stops immediately
- Remaining hooks in that phase don't run
- For pre-dispatch: the handler never executes
- For post phases: the rendered output is discarded
- The error message is returned

```rust
use standout_dispatch::HookError;

// Create error with phase context
HookError::pre_dispatch("database connection failed")

// With source error for debugging
HookError::post_dispatch("transformation failed")
    .with_source(underlying_error)
```

---

## Render Handlers

The render handler is a pluggable callback that converts data to output:

```rust
use standout_dispatch::{from_fn, RenderFn};

// Simple JSON renderer
let render: RenderFn = from_fn(|data, _view| {
    Ok(serde_json::to_string_pretty(data)?)
});
```

### Render Function Signature

```rust
fn(&serde_json::Value, &str) -> Result<String, RenderError>
```

Parameters:
- `data`: The serialized handler output
- `view`: A view/template name hint (can be ignored)

### Using View Names

The `view` parameter enables template-based rendering:

```rust
let render = from_fn(move |data, view| {
    match view {
        "list" => format_as_list(data),
        "detail" => format_as_detail(data),
        _ => Ok(serde_json::to_string_pretty(data)?),
    }
});
```

> **For standout framework users:** The framework automatically maps view names to template files. See standout documentation for details.

### Local Render Functions

For render functions that need mutable state:

```rust
use standout_dispatch::{from_fn_mut, LocalRenderFn};

let render: LocalRenderFn = from_fn_mut(|data, view| {
    // Can capture and mutate state
    Ok(format_data(data))
});
```

---

## Default Command Support

Handle the case when no subcommand is specified:

```rust
use standout_dispatch::{has_subcommand, insert_default_command};

let matches = cmd.get_matches_from(args);

if !has_subcommand(&matches) {
    // Re-parse with default command inserted
    let args_with_default = insert_default_command(std::env::args(), "list");
    let matches = cmd.get_matches_from(args_with_default);
    // Now dispatch to "list"
}
```

`insert_default_command` inserts the command name after the binary name but before any flags.

---

## Putting It Together

A complete dispatch flow:

```rust
use standout_dispatch::{
    SimpleFnHandler, FnHandler, Output, CommandContext, Hooks, HookError,
    from_fn, extract_command_path, get_deepest_matches, path_to_string,
};

fn main() -> anyhow::Result<()> {
    // 1. Define clap command
    let cmd = Command::new("myapp")
        .subcommand(Command::new("list"))
        .subcommand(Command::new("delete").arg(Arg::new("id").required(true)));

    // 2. Create handlers
    // SimpleFnHandler: for handlers that don't need CommandContext
    let list_handler = SimpleFnHandler::new(|_m| {
        storage::list()  // Result<T, E> auto-wraps in Output::Render
    });

    // FnHandler: when you need CommandContext
    let delete_handler = FnHandler::new(|matches, _ctx| {
        let id: &String = matches.get_one("id").unwrap();
        storage::delete(id)?;
        Ok(Output::Silent)
    });

    // 3. Create render function
    let render = from_fn(|data, _view| {
        Ok(serde_json::to_string_pretty(data)?)
    });

    // 4. Create hooks
    let hooks = Hooks::new()
        .pre_dispatch(|_m, _ctx| {
            println!("Starting command...");
            Ok(())
        });

    // 5. Parse and dispatch
    let matches = cmd.get_matches();
    let path = extract_command_path(&matches);
    let mut ctx = CommandContext {
        command_path: path.clone(),
        ..Default::default()
    };

    // Run pre-dispatch hooks (may inject state via ctx.extensions)
    hooks.run_pre_dispatch(&matches, &mut ctx)?;

    // Dispatch based on command
    let result = match path_to_string(&path).as_str() {
        "list" => {
            let output = list_handler.handle(&matches, &ctx)?;
            if let Output::Render(data) = output {
                let json = serde_json::to_value(&data)?;
                let rendered = render(&json, "list")?;
                println!("{}", rendered);
            }
        }
        "delete" => {
            let deep = get_deepest_matches(&matches);
            delete_handler.handle(deep, &ctx)?;
            println!("Deleted.");
        }
        _ => eprintln!("Unknown command"),
    };

    Ok(())
}
```

---

## Summary

The execution model provides:

1. **Clear pipeline** — Each stage has defined inputs and outputs
2. **Hook points** — Intercept before, after handler, and after render
3. **Command routing** — Utilities for navigating subcommand hierarchies
4. **Pluggable rendering** — Render functions are separate from handlers
5. **Testable stages** — Each component can be tested in isolation