use std::collections::HashMap;
use serde_json::Value;
#[derive(Debug)]
pub enum DispatchError {
UnknownCommand(String),
BadArgs(String),
}
pub struct Ctx {
pub user: String,
}
pub type HandlerFn = fn(&Ctx, &[String]) -> Result<Value, DispatchError>;
macro_rules! define_dispatch {
($($name:literal => $handler:path),+ $(,)?) => {
pub struct Router {
table: HashMap<&'static str, HandlerFn>,
}
impl Router {
pub fn new() -> Self {
let mut table: HashMap<&'static str, HandlerFn> = HashMap::new();
$( table.insert($name, $handler as HandlerFn); )+
Router { table }
}
pub fn commands(&self) -> Vec<&'static str> {
let mut names: Vec<&'static str> = self.table.keys().copied().collect();
names.sort_unstable();
names
}
pub fn dispatch(&self, ctx: &Ctx, cmd: &str, args: &[String]) -> Result<Value, DispatchError> {
match self.table.get(cmd) {
Some(handler) => handler(ctx, args),
None => Err(DispatchError::UnknownCommand(format!(
"unknown command '{}'; available: {}",
cmd,
self.commands().join(", ")
))),
}
}
}
};
}
define_dispatch! {
"ping" => crate::handlers::ping,
"echo" => crate::handlers::echo,
"set_flag" => crate::handlers::set_flag,
}
use serde_json::{json, Value};
use crate::{Ctx, DispatchError};
pub fn ping(_ctx: &Ctx, _args: &[String]) -> Result<Value, DispatchError> {
Ok(json!({ "pong": true }))
}
pub fn echo(_ctx: &Ctx, args: &[String]) -> Result<Value, DispatchError> {
Ok(json!({ "echo": args.join(" ") }))
}
pub fn set_flag(ctx: &Ctx, args: &[String]) -> Result<Value, DispatchError> {
let name = args
.get(0)
.ok_or_else(|| DispatchError::BadArgs("set_flag requires <name>".into()))?;
let raw = args
.get(1)
.ok_or_else(|| DispatchError::BadArgs("set_flag requires <value>".into()))?;
let value: bool = raw
.parse()
.map_err(|_| DispatchError::BadArgs(format!("'{}' is not a bool", raw)))?;
Ok(json!({
"flag": name,
"value": value,
"set_by": ctx.user,
}))
}
mod handlers;
use std::collections::HashMap;
use serde_json::Value;
#[macro_use]
mod dispatch;
pub use dispatch::{Ctx, DispatchError, HandlerFn, Router};
fn run(router: &Router, ctx: &Ctx, line: &str) {
let mut parts = line.split_whitespace().map(String::from);
let cmd = match parts.next() {
Some(c) => c,
None => return,
};
let args: Vec<String> = parts.collect();
match router.dispatch(ctx, &cmd, &args) {
Ok(value) => println!("ok: {}", value),
Err(DispatchError::UnknownCommand(msg)) => eprintln!("error: {}", msg),
Err(DispatchError::BadArgs(msg)) => eprintln!("bad args: {}", msg),
}
}
fn main() {
let router = Router::new();
let ctx = Ctx { user: "alice".into() };
run(&router, &ctx, "ping");
run(&router, &ctx, "echo hello world");
run(&router, &ctx, "set_flag beta true");
run(&router, &ctx, "set_flag beta maybe");
run(&router, &ctx, "reboot now");
}
This snippet shows how to route incoming commands to their handlers using a declarative macro that generates a static dispatch table at compile time, avoiding the boilerplate match arm per command that grows unmaintainable as a service accumulates dozens of verbs.
The core idea in dispatch macro is that each command maps to a name string and a handler function with a uniform signature. Rather than writing a giant match by hand, define_dispatch! expands into a Router struct holding a HashMap<&'static str, HandlerFn>, where HandlerFn is a boxed function pointer. Because the macro emits the registration entries, adding a command is a one-line change and the table stays in sync with the actual functions. The macro also generates a commands() helper so callers can enumerate what is available, which is handy for a help command or usage errors.
The uniform signature is the crux of the pattern: every handler takes a parsed Ctx plus raw args and returns Result<Value, DispatchError>. Keeping the signature identical is what lets heterogeneous handlers live in one map — the trade-off is that argument parsing moves inside each handler rather than being expressed in the type system, so a mistyped argument surfaces at runtime as a DispatchError::BadArgs rather than at compile time.
In handlers, the concrete functions ping, echo, and set_flag implement that contract. They pull values out of args, validate them, and construct a serde_json::Value result. Note how set_flag returns DispatchError::BadArgs early when an expected argument is missing — the router never has to know the details.
In main, Router::new() builds the table once, then dispatch looks up the command name and either invokes the handler or returns DispatchError::UnknownCommand, whose message includes the list from commands(). The lookup is an amortized O(1) HashMap hit rather than a linear scan of match arms.
A developer reaches for this when the command set is large or plugin-like and readability of a hand-written match degrades. The pitfall is the loss of exhaustiveness checking that a real match gives, so tests that assert every registered name resolves become important. For a fixed, small set of commands, a plain match is simpler and should be preferred.
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
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
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
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
Share this code
Here's the card — post it anywhere.