standout-dispatch 7.4.0

Command dispatch and routing for clap-based CLIs
Documentation
# Partial Adoption

One of the key benefits of `standout-dispatch` is that you don't need to adopt it all at once. You can migrate one command at a time, keeping existing code alongside dispatch-managed commands.

---

## The Problem with All-or-Nothing Frameworks

Many CLI frameworks require a complete rewrite:

- All commands must use the framework's patterns
- Existing code can't coexist with framework code
- Migration is a massive undertaking
- Risk is concentrated in a single change

`standout-dispatch` is designed differently. It's a library, not a framework—you call it, it doesn't call you.

---

## Strategy: Migrate One Command at a Time

### Step 1: Identify a Good Starting Command

Pick a command that:
- Is self-contained (few dependencies on other commands)
- Has clear inputs and outputs
- Would benefit from structured output (JSON, etc.)
- Has existing tests you can update

### Step 2: Create the Handler

Convert the command's logic to a handler:

```rust
// Before: mixed logic and output
fn list_command(matches: &ArgMatches) {
    let items = storage::list().unwrap();
    for item in items {
        println!("{}: {}", item.id, item.name);
    }
}

// After: handler returns data
fn list_handler(_m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<Vec<Item>> {
    let items = storage::list()?;
    Ok(Output::Render(items))
}
```

### Step 3: Set Up Dispatch for That Command

```rust
use standout_dispatch::{FnHandler, from_fn, extract_command_path, path_to_string};

fn main() {
    let cmd = build_clap_command();  // Your existing clap definition
    let matches = cmd.get_matches();
    let path = extract_command_path(&matches);

    // Dispatch-managed command
    if path_to_string(&path) == "list" {
        let handler = FnHandler::new(list_handler);
        let render = from_fn(|data, _| Ok(serde_json::to_string_pretty(data)?));

        let ctx = CommandContext { command_path: path };
        if let Ok(Output::Render(data)) = handler.handle(&matches, &ctx) {
            let json = serde_json::to_value(&data).unwrap();
            println!("{}", render(&json, "list").unwrap());
        }
        return;
    }

    // Fall back to existing code for other commands
    match matches.subcommand() {
        Some(("add", sub)) => add_command(sub),
        Some(("delete", sub)) => delete_command(sub),
        _ => {}
    }
}
```

### Step 4: Repeat

Migrate one command at a time. Each migration:
- Is a small, reviewable change
- Can be tested independently
- Doesn't affect other commands
- Is easy to roll back if needed

---

## Coexistence Patterns

### Pattern 1: Check Path First

```rust
let path = extract_command_path(&matches);

// Dispatch-managed commands
let dispatch_commands = ["list", "show", "export"];
if dispatch_commands.contains(&path_to_string(&path).as_str()) {
    dispatch_command(&matches, &path);
    return;
}

// Legacy commands
legacy_dispatch(&matches);
```

### Pattern 2: Try Dispatch, Fall Back

```rust
if let Some(result) = try_dispatch(&matches) {
    handle_dispatch_result(result);
} else {
    // Not a dispatch-managed command
    legacy_dispatch(&matches);
}
```

### Pattern 3: Wrapper Function

```rust
fn run_command(matches: &ArgMatches) {
    let path = extract_command_path(matches);

    match path_to_string(&path).as_str() {
        // New dispatch-based handlers
        "list" => run_with_dispatch(list_handler, matches, &path),
        "show" => run_with_dispatch(show_handler, matches, &path),

        // Legacy handlers (unchanged)
        "add" => add_command(get_deepest_matches(matches)),
        "delete" => delete_command(get_deepest_matches(matches)),

        _ => eprintln!("Unknown command"),
    }
}

fn run_with_dispatch<T: Serialize>(
    handler: impl Fn(&ArgMatches, &CommandContext) -> HandlerResult<T>,
    matches: &ArgMatches,
    path: &[String],
) {
    let ctx = CommandContext { command_path: path.to_vec() };
    match handler(matches, &ctx) {
        Ok(Output::Render(data)) => {
            let json = serde_json::to_value(&data).unwrap();
            println!("{}", serde_json::to_string_pretty(&json).unwrap());
        }
        Ok(Output::Silent) => {}
        Ok(Output::Binary { data, filename }) => {
            std::fs::write(&filename, &data).unwrap();
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}
```

---

## Benefits During Migration

### Immediate Benefits per Command

Each migrated command gains:

1. **Structured output** — JSON/YAML support
2. **Testable logic** — Handler is a pure function
3. **Error handling**`?` operator, proper error types
4. **Hook points** — Add logging, auth without touching handler

### Progressive Enhancement

As you migrate more commands:

1. **Shared hooks** — Apply auth check to all migrated commands
2. **Consistent output** — Same renderer for all commands
3. **Unified error handling** — Errors formatted consistently

---

## Migration Checklist

For each command:

- [ ] Create data types (`#[derive(Serialize)]`)
- [ ] Write handler function
- [ ] Add to dispatch routing
- [ ] Update tests to test handler directly
- [ ] Verify existing behavior unchanged
- [ ] Document the migration

---

## Example: Full Migration

Before (monolithic):

```rust
fn main() {
    let matches = build_cli().get_matches();

    match matches.subcommand() {
        Some(("list", sub)) => list_command(sub),
        Some(("add", sub)) => add_command(sub),
        Some(("delete", sub)) => delete_command(sub),
        Some(("export", sub)) => export_command(sub),
        _ => {}
    }
}
```

After (gradual migration):

```rust
fn main() {
    let matches = build_cli().get_matches();
    let path = extract_command_path(&matches);

    // Dispatch-managed (migrated)
    if let Some(result) = dispatch_if_managed(&matches, &path) {
        return;
    }

    // Legacy (not yet migrated)
    match matches.subcommand() {
        Some(("add", sub)) => add_command(sub),
        Some(("delete", sub)) => delete_command(sub),
        _ => {}
    }
}

fn dispatch_if_managed(matches: &ArgMatches, path: &[String]) -> Option<()> {
    let ctx = CommandContext { command_path: path.to_vec() };
    let render = from_fn(|data, _| Ok(serde_json::to_string_pretty(data)?));

    let result = match path_to_string(path).as_str() {
        "list" => list_handler(matches, &ctx),
        "export" => export_handler(matches, &ctx),
        _ => return None,  // Not managed by dispatch
    };

    match result {
        Ok(Output::Render(data)) => {
            let json = serde_json::to_value(&data).ok()?;
            println!("{}", render(&json, "").ok()?);
        }
        Ok(Output::Silent) => {}
        Ok(Output::Binary { data, filename }) => {
            std::fs::write(&filename, &data).ok()?;
        }
        Err(e) => eprintln!("Error: {}", e),
    }

    Some(())
}
```

---

## Summary

Partial adoption lets you:

1. **Start small** — Migrate one command at a time
2. **Reduce risk** — Each migration is independent
3. **Maintain velocity** — Keep shipping while migrating
4. **Validate benefits** — See the value before full commitment

The goal is pragmatic improvement, not architectural purity. Migrate what benefits most, leave what works alone.