standout-dispatch 10.0.0

Command dispatch and routing for clap-based CLIs
Documentation
# Execution Model

`standout-dispatch` manages a strict linear pipeline from CLI input to rendered
output. This explicitly separated flow keeps handler adapters decoupled from
presentation (renderers) and shell pipeline 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 CLI adapter executes. It receives `ArgMatches` and
`CommandContext`, calls the CLI-free application library, and returns a
`HandlerResult<T>`—either data to render, a silent marker, or binary content.
For simpler adapters, use the `#[handler]` macro to write typed 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](../../pipe/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 [App State and Extensions](app-state.md#extensions-per-request-state) for the `Extensions` API and 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(TextOutput)`, `Binary(Vec<u8>, String)`, `Artifact(ArtifactOutput)`, or `Silent`. For an artifact the bytes and report are still pre-write, so a hook may transform them via `as_artifact_mut()`; the framework, not the hook, performs the final write.

```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_name("export")
               // Filter through jq (capture mode)
               .pipe_through("jq '.items'")
        })
        .command_with("copy", handlers::copy, |cfg| {
            cfg.template_name("copy")
               // Send to clipboard (consume mode)
               .pipe_to_clipboard()
        })
        .command_with("debug", handlers::debug, |cfg| {
            cfg.template_name("debug")
               // 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](../../pipe/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)
```

---

## 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,
    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 hooks
    let hooks = Hooks::new()
        .pre_dispatch(|_m, _ctx| {
            println!("Starting command...");
            Ok(())
        });

    // 4. 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 {
                println!("{}", serde_json::to_string_pretty(&data)?);
            }
        }
        "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. **Presentation ownership** — Rendering stays outside dispatch handlers
5. **Testable stages** — Each component can be tested in isolation