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:
#![allow(unused)] fn main() { // 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
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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { if let Some(result) = try_dispatch(&matches) { handle_dispatch_result(result); } else { // Not a dispatch-managed command legacy_dispatch(&matches); } }
Pattern 3: Wrapper Function
#![allow(unused)] fn main() { 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:
- Structured output — JSON/YAML support
- Testable logic — Handler is a pure function
- Error handling —
?operator, proper error types - Hook points — Add logging, auth without touching handler
Progressive Enhancement
As you migrate more commands:
- Shared hooks — Apply auth check to all migrated commands
- Consistent output — Same renderer for all commands
- 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):
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):
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:
- Start small — Migrate one command at a time
- Reduce risk — Each migration is independent
- Maintain velocity — Keep shipping while migrating
- Validate benefits — See the value before full commitment
The goal is pragmatic improvement, not architectural purity. Migrate what benefits most, leave what works alone.