cmdreg 0.2.1

A lightweight string-keyed command dispatcher with axum-style extractors for Rust
Documentation

cmdreg

Crates.io docs.rs CI License: MIT

English | 中文

A lightweight, string-keyed command dispatcher for Rust with axum-style extractors and optional auto-registration macros.

Designed for applications that dispatch operations by name — such as IPC bridges, plugin systems, FFI layers, or embedded scripting hosts.

Features

  • String-keyed dispatch — register and invoke handlers by name ("fs.read", "workspace.open")
  • Axum-style extractors — use Json(args) to automatically deserialize arguments from JSON
  • Sync & async handlers — first-class support for both, in a unified API
  • #[command] macro — annotate functions to auto-register them (opt-in via macros feature)
  • Callback commands — register and invoke callback-style handlers
  • Zero boilerplate — no traits to implement on your handler functions

Quick Start

[dependencies]
cmdreg = "0.2"
anyhow = "1.0"

Register & invoke commands manually

use cmdreg::{reg_command, reg_command_async, invoke_command, invoke_command_async, CommandContext};

// Sync handler
fn hello() -> cmdreg::CommandResult {
    cmdreg::CommandResponse::json("hello world")
}

reg_command("app.hello", hello).unwrap();

let result = invoke_command("app.hello", CommandContext::None).unwrap();

// Async handler
async fn greet(cmdreg::Json(name): cmdreg::Json<String>) -> cmdreg::CommandResult {
    cmdreg::CommandResponse::json(format!("hello, {}", name))
}

reg_command_async("app.greet", greet).unwrap();

Auto-registration with #[command] macro

Enable the macros feature:

[dependencies]
cmdreg = { version = "0.1", features = ["macros"] }

Classic style (extractor-based)

Use Json<T> extractors and return CommandResult explicitly:

use cmdreg::{command, Json, CommandResult, CommandResponse};

#[command("fs")]
fn exists(Json(path): Json<String>) -> CommandResult {
    let exists = std::path::Path::new(&path).exists();
    CommandResponse::json(exists)
}

#[command("fs")]
async fn read_file(Json(path): Json<String>) -> CommandResult {
    let content = tokio::fs::read_to_string(&path).await?;
    CommandResponse::json(content)
}

Plain style (auto-generated)

Use plain parameters and any Serialize return type — the macro auto-generates a #[derive(Deserialize)] args struct and wraps the return value with CommandResponse::json():

use cmdreg::command;

#[command("fs")]
fn get_file_list(path: String, recursive: bool) -> Vec<String> {
    // caller passes: {"path": "./src", "recursive": true}
    vec![]
}

#[command("math")]
fn divide(a: f64, b: f64) -> anyhow::Result<f64> {
    if b == 0.0 { anyhow::bail!("division by zero"); }
    Ok(a / b)
}

#[command]
fn get_version() -> String {
    "1.0.0".to_string()
}

Supported return types in plain style:

Return type Behavior
T: Serialize Wrapped with CommandResponse::json(value)
Result<T: Serialize> Unwrapped with ?, then wrapped with json
CommandResult Passed through directly
() / no return Returns Ok(CommandResponse::None)

Note: Plain-style parameters must be owned types (e.g. String, not &str). Reference types cannot be deserialized from JSON and will produce a compile error.

// At startup:
fn main() {
    cmdreg::reg_all_commands().unwrap();
    // "fs.get_file_list", "math.divide", "get_version", etc. are registered
}

Global Configuration

Set a default rename_all for all #[command] macros in your crate's Cargo.toml:

[package.metadata.cmdreg]
rename_all = "camelCase"

The proc-macro reads Cargo.toml at compile time. Without this config, field names match Rust parameter names as-is.

Per-function rename_all overrides the global default:

#[command("fs", rename_all = "camelCase")]
fn get_file_list(file_path: String, is_recursive: bool) -> Vec<String> {
    // JSON: {"filePath": "...", "isRecursive": true}
    vec![]
}

#[command("stats", rename_all = "SCREAMING_SNAKE_CASE")]
fn get_total_count(my_value: i32) -> i32 {
    // JSON: {"MY_VALUE": 42} — overrides global default
    my_value
}

Feature Flags

Feature Default Description
macros off Enables #[command] macro and reg_all_commands()
full off Enables all optional features

How It Works

  1. CommandMap<K, F> — a generic HashMap wrapper keyed by string-like types.
  2. Global registriesLazyLock<Arc<RwLock<...>>> singletons for sync, async, and callback commands.
  3. Handler traitsCommandHandler<T> (sync) and CommandHandlerAsync<T> (async) are auto-implemented for functions with up to 10 extractor parameters.
  4. ExtractorsJson<T> deserializes CommandContext into your typed arguments, similar to axum's extractor pattern.
  5. #[command("prefix")] / #[command] — a proc-macro that generates a registration function and submits it to inventory for collection at link time. When no prefix is given, the function name is used directly as the command key.
  6. Plain-style support — when using plain parameters (e.g. path: String) instead of Json<T> extractors, the macro auto-generates a #[derive(Deserialize)] args struct and wraps the return value as JSON.

Requirements

  • Rust nightly (uses closure_lifetime_binder)
  • tokio runtime (for async command dispatch)

License

MIT