rust 116 lines · 3 tabs

Compile-Time Command Dispatch Table With a Rust Declarative Macro

Shared by codesnips Sep 2026
3 tabs
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,
}
3 files · rust Explain with highlit

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

Share this code

Here's the card — post it anywhere.

Compile-Time Command Dispatch Table With a Rust Declarative Macro — share card
Link copied