standout-dispatch 3.4.0

Command dispatch and routing for clap-based CLIs
Documentation

standout-dispatch

Command dispatch with strict separation of logic and presentation for CLI applications.

use standout_dispatch::{Handler, Output, CommandContext, from_fn};

// Handler returns data, not strings
fn list_handler(_m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<Vec<Task>> {
    let tasks = db::fetch_tasks()?;
    Ok(Output::Render(tasks))
}

// Renderer is pluggable—you decide how to format
let render = from_fn(|data| Ok(serde_json::to_string_pretty(data)?));

Why standout-dispatch?

CLI commands typically mix business logic with output formatting: database queries interleaved with println!, validation tangled with ANSI codes, error handling scattered across presentation. The result is code that's hard to test, hard to change, and impossible to reuse.

standout-dispatch enforces a clean separation:

CLI args → Handler (logic) → Data → Renderer (presentation) → Output
  • Handlers receive parsed arguments, return serializable data
  • Renderers are pluggable callbacks you provide
  • Hooks intercept execution at defined points

This isn't just architectural nicety—it unlocks:

  • Testable handlers — Pure functions with explicit inputs and outputs
  • Swappable renderers — JSON, templates, plain text from the same handler
  • Cross-cutting concerns — Auth, logging, transformation via hooks
  • Incremental adoption — Migrate one command at a time

Features

Handler Traits

Thread-safe and local variants for different use cases:

// Thread-safe handler (Send + Sync, &self)
impl Handler for MyHandler {
    type Output = Data;
    fn handle(&self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Data> {
        Ok(Output::Render(self.db.query()?))
    }
}

// Local handler for mutable state (&mut self, no Send + Sync)
impl LocalHandler for MyCache {
    type Output = Data;
    fn handle(&mut self, matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Data> {
        self.invalidate();  // &mut self works
        Ok(Output::Render(self.get()?))
    }
}

Pluggable Render Handlers

Dispatch doesn't know how rendering works—you provide a closure:

use standout_dispatch::from_fn;

// JSON output
let json_render = from_fn(|data| Ok(serde_json::to_string_pretty(data)?));

// Custom formatting
let custom_render = from_fn(|data| {
    let name = data["name"].as_str().unwrap_or("unknown");
    Ok(format!("Result: {}", name))
});

// Template-based (with standout-render or any engine)
let format = OutputFormat::from_cli_args(&matches);
let template_render = from_fn(move |data| {
    my_renderer::render(data, format)
});

This design means dispatch orchestrates execution without coupling to any rendering implementation.

Hook System

Intercept execution at three points:

use standout_dispatch::{Hooks, HookError, RenderedOutput};

let hooks = Hooks::new()
    // Before handler: validation, auth
    .pre_dispatch(|matches, ctx| {
        if !is_authenticated() {
            return Err(HookError::pre_dispatch("auth required"));
        }
        Ok(())
    })
    // After handler, before render: transform data
    .post_dispatch(|_m, _ctx, mut data| {
        data["timestamp"] = json!(Utc::now().to_rfc3339());
        Ok(data)
    })
    // After render: transform output
    .post_output(|_m, _ctx, output| {
        if let RenderedOutput::Text(s) = output {
            Ok(RenderedOutput::Text(format!("{}\n-- footer", s)))
        } else {
            Ok(output)
        }
    });

Hooks chain—each receives the output of the previous.

Command Routing Utilities

Extract and navigate clap's ArgMatches:

use standout_dispatch::{
    extract_command_path,
    get_deepest_matches,
    has_subcommand,
    insert_default_command,
};

// myapp db migrate --steps 5
let path = extract_command_path(&matches);  // ["db", "migrate"]
let deep = get_deepest_matches(&matches);   // ArgMatches for "migrate"

// Default command support
if !has_subcommand(&matches) {
    let args = insert_default_command(std::env::args(), "list");
    // Reparse with default command inserted
}

Output Types

Handlers produce one of three outputs:

pub enum Output<T: Serialize> {
    Render(T),                              // Data for rendering
    Silent,                                 // No output (side-effect only)
    Binary { data: Vec<u8>, filename: String }, // File export
}

Quick Start

[dependencies]
standout-dispatch = "2.1"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
use standout_dispatch::{
    FnHandler, HandlerResult, Output, CommandContext,
    Hooks, from_fn, extract_command_path, path_to_string,
};
use clap::{Command, Arg};
use serde::Serialize;
use std::collections::HashMap;

#[derive(Serialize)]
struct ListResult { items: Vec<String> }

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

    // 2. Create handler
    let list_handler = FnHandler::new(|_m, _ctx| {
        Ok(Output::Render(ListResult {
            items: vec!["task-1".into(), "task-2".into()],
        }))
    });

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

    // 4. Build registry and dispatch
    let matches = cmd.get_matches();
    let path = extract_command_path(&matches);

    if path_to_string(&path) == "list" {
        let ctx = CommandContext { command_path: path };
        let result = list_handler.handle(&matches, &ctx)?;

        if let Output::Render(data) = result {
            let json = serde_json::to_value(&data)?;
            let output = render(&json)?;
            println!("{}", output);
        }
    }

    Ok(())
}

Documentation

Guides

Topics

Reference

Used By

This crate provides the dispatch foundation for the standout CLI framework, which combines dispatch with standout-render for a complete CLI solution. Use standout-dispatch directly when you want the separation pattern without the rendering layer.

License

MIT