use std::any::Any;
pub trait CommandHandler {
fn name(&self) -> &str;
fn handle(&self, input: &dyn Any) -> Box<dyn Any>;
}
pub trait Command: Any {
fn as_any(&self) -> &dyn Any;
}
impl<T: Any> Command for T {
fn as_any(&self) -> &dyn Any {
self
}
}
pub fn expect_input<'a, T: Any>(input: &'a dyn Any) -> &'a T {
input
.downcast_ref::<T>()
.expect("command payload type did not match handler")
}
use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use crate::handler::CommandHandler;
#[derive(Debug)]
pub enum DispatchError {
UnknownCommand(String),
}
impl fmt::Display for DispatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DispatchError::UnknownCommand(name) => {
write!(f, "no handler registered for command '{}'", name)
}
}
}
}
#[derive(Default)]
pub struct Registry {
handlers: HashMap<String, Box<dyn CommandHandler>>,
}
impl Registry {
pub fn new() -> Self {
Registry {
handlers: HashMap::new(),
}
}
pub fn register<H: CommandHandler + 'static>(&mut self, handler: H) {
self.handlers.insert(handler.name().to_string(), Box::new(handler));
}
pub fn dispatch(&self, name: &str, input: &dyn Any) -> Result<Box<dyn Any>, DispatchError> {
match self.handlers.get(name) {
Some(handler) => Ok(handler.handle(input)),
None => Err(DispatchError::UnknownCommand(name.to_string())),
}
}
}
mod handler;
mod registry;
use std::any::Any;
use handler::{expect_input, CommandHandler};
use registry::Registry;
struct EchoHandler;
impl CommandHandler for EchoHandler {
fn name(&self) -> &str {
"echo"
}
fn handle(&self, input: &dyn Any) -> Box<dyn Any> {
let text = expect_input::<String>(input);
Box::new(format!("echo: {}", text))
}
}
struct SumHandler;
impl CommandHandler for SumHandler {
fn name(&self) -> &str {
"sum"
}
fn handle(&self, input: &dyn Any) -> Box<dyn Any> {
let nums = expect_input::<Vec<i64>>(input);
Box::new(nums.iter().sum::<i64>())
}
}
fn main() {
let mut registry = Registry::new();
registry.register(EchoHandler);
registry.register(SumHandler);
let echoed = registry.dispatch("echo", &String::from("hello")).unwrap();
if let Some(text) = echoed.downcast_ref::<String>() {
println!("{}", text);
}
let total = registry.dispatch("sum", &vec![1i64, 2, 3, 4]).unwrap();
if let Some(value) = total.downcast_ref::<i64>() {
println!("total = {}", value);
}
match registry.dispatch("missing", &0i64) {
Ok(_) => unreachable!(),
Err(err) => eprintln!("{}", err),
}
}
This snippet shows how a command dispatcher can be built around trait objects so that new command handlers plug into a central registry without the dispatcher knowing their concrete types. The problem it solves is decoupling: a subsystem wants to route named commands to handlers, but the set of handlers should be extensible and each may take a different payload and produce a different result.
In handler.rs, the core abstraction is the object-safe CommandHandler trait. Object safety is the constraint that makes Box<dyn CommandHandler> possible, so the trait avoids generic methods and instead operates on erased inputs: handle receives a &dyn Any and returns Box<dyn Any>. A name method supplies the registry key. The Command and helper traits keep the payload-to-Any conversion in one place. Using Any is the deliberate trade-off here — the caller regains concrete types through downcast, paying a small runtime check in exchange for a heterogeneous, runtime-composed handler set.
In registry.rs, the Registry wraps a HashMap<String, Box<dyn CommandHandler>>. The register method takes any H: CommandHandler + 'static, boxes it, and stores it under its own name(), so registration order and call sites stay decoupled. The dispatch method looks up the handler and calls handle, returning a DispatchError when the command name is unknown. Because the map owns boxed trait objects, the concrete handler types are fully erased after registration; only the trait vtable remains.
In main.rs, two concrete handlers, EchoHandler and SumHandler, implement CommandHandler with different payload and output types. They are registered into one Registry, then dispatched by name. The results come back as Box<dyn Any> and are recovered with downcast_ref, which returns None if the actual type does not match — a real pitfall to guard against when payloads and handlers drift apart. This pattern is worth reaching for when the handler set is open-ended (plugins, extensions, message routers); when the set is closed and known at compile time, an enum with a match is usually simpler and avoids the Any erasure entirely.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Called {}", stringify!($func_name));
}
};
Declarative macros (macro_rules!) for code generation
Share this code
Here's the card — post it anywhere.