dispatch_map

Crates.iodispatch_map
lib.rsdispatch_map
version0.1.3
created_at2025-07-24 15:07:32.128803+00
updated_at2025-07-26 10:37:35.274568+00
descriptionType-safe, declarative dispatch maps for Rust configuration with automatic glue and zero boilerplate.
homepagehttps://github.com/BppleMan/dispatch_map
repositoryhttps://github.com/BppleMan/dispatch_map
max_upload_size
id1766172
size21,677
BppleMan (BppleMan)

documentation

https://docs.rs/dispatch_map

README

dispatch_map  Crates.io License Build Docs.rs

💡 Polymorphic Map dispatch, declarative deserialization, type safety, business structure and configuration perfectly aligned!


🚀 Background & Motivation

You are developing an aggregate payment platform, and each payment channel has its own unique Rust config type, for example:

enum PaymentChannel {
    Stripe,
    AliPay,
    PayPal,
}

struct StripeConfig {
    api_key: String,
    region: String
}
struct AliPayConfig {
    app_id: String,
    private_key: String
}
struct PayPalConfig {
    client_id: String,
    client_secret: String
}

enum ChannelConfig {
    Stripe(StripeConfig),
    AliPay(AliPayConfig),
    PayPal(PayPalConfig),
}

struct AppConfig {
    channels: HashMap<PaymentChannel, ChannelConfig>
}

You want to configure these structures in .toml/.yaml/.json files, achieving "type-driven, clear structure".


⚠️ Pain Point: The Disaster of Default Serialization Structure

When using #[derive(Serialize, Deserialize)] directly, you expect to get:

[channels.Stripe]
api_key = "sk_test_123"
region = "us"

But what you actually get is:

[channels.Stripe.Stripe]
api_key = "sk_test_123"
region = "us"

Each enum layer adds an extra nesting!
Maintaining this is extremely redundant, and deserialization becomes mechanical and repetitive.


🛠️ Traditional Solution: Handwritten Glue

You have to write such "deserialization glue":

fn deserialize_channels<'de, D>(deserializer: D) -> Result<HashMap<PaymentChannel, ChannelConfig>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let table = toml::Value::deserialize(deserializer)?;
    let map = table.as_table().ok_or_else(|| de::Error::custom("channels should be a table"))?;
    let mut result = HashMap::new();
    for (key, value) in map {
        let channel: PaymentChannel = key.parse().map_err(de::Error::custom)?;
        let config = match channel {
            PaymentChannel::Stripe => ChannelConfig::Stripe(StripeConfig::deserialize(value)?),
            PaymentChannel::AliPay => ChannelConfig::AliPay(AliPayConfig::deserialize(value)?),
            PaymentChannel::PayPal => ChannelConfig::PayPal(PayPalConfig::deserialize(value)?),
        };
        result.insert(channel, config);
    }
    Ok(result)
}
  • Each channel needs a handwritten match, and maintenance cost increases rapidly as types grow.
  • Serialization and deserialization are hard to make reversible, and it's easy to make type errors or miss branches.

✨ Advanced Solution: Use #[serde(untagged)] to Optimize Nesting

You can add one line to ChannelConfig:

#[serde(untagged)]
enum ChannelConfig {
    Stripe(StripeConfig),
    AliPay(AliPayConfig),
    PayPal(PayPalConfig),
}

This lets serde automatically unbox, leaving only one layer of nesting and a clean config structure.


🧩 dispatch_map: Declarative Dispatch, Ultimate Simplicity

With dispatch_map, you can declare glue in one line, all type dispatch is automatic, no need to write tedious match by hand.

1️⃣ Type declarations are as elegant as business modeling

use dispatch_map::{DispatchMap, DispatchSeed};
use dispatch_map_derive::{DispatchKey, dispatch_pattern};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DispatchKey)]
pub enum PaymentChannel {
    Stripe,
    AliPay,
    PayPal,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChannelConfig {
    Stripe(StripeConfig),
    AliPay(AliPayConfig),
    PayPal(PayPalConfig),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StripeConfig {
    pub api_key: String,
    pub region: String,
}

// Other channels similar...

2️⃣ Glue dispatch only needs one macro line, type safe

dispatch_pattern! {
    PaymentChannel::Stripe  => ChannelConfig::Stripe(StripeConfig),
    PaymentChannel::AliPay  => ChannelConfig::AliPay(AliPayConfig),
    PaymentChannel::PayPal  => ChannelConfig::PayPal(PayPalConfig),
}

3️⃣ Business config uses DispatchMap, no need for custom deserializer

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppConfig {
    pub channels: DispatchMap<PaymentChannel, ChannelConfig>,
}

🎯 Advantages at a Glance

  • Type safety: Type-driven throughout, no more handwritten match.
  • Minimal glue: Adding a new channel only needs one more line, no redundant code to maintain.
  • Reversible serialization/deserialization: Clean config structure, format and business types perfectly aligned.
  • Generic friendly, IDE completion always available.
  • Compatible with main serde formats: toml, yaml, json all supported.
  • No performance loss: All glue is generated at compile time, no runtime dispatch.

🧪 Complete Example (as unit test)

let src = r#"
[channels.Stripe]
api_key = "sk_test_123"
region = "us"

[channels.AliPay]
app_id = "2023"
private_key = "..."
"#;

let cfg: AppConfig = toml::from_str(src).unwrap();

assert!(matches!(cfg.channels.get(&PaymentChannel::Stripe), Some(ChannelConfig::Stripe(_))));

💡 FAQ

  • Q: Does it support other key/value types?
    As long as the key implements Eq + Hash + Deserialize + Serialize, and the value can be glued, it works.
  • Q: Does it support multi-level nesting/recursive structures?
    Any combination and nesting is possible, fully generic friendly.
  • Q: Compatible with serde features?
    Fully compatible with the serde ecosystem.

🏆 Summary

dispatch_map makes polymorphic config serialization/deserialization simple and type-safe, declaration is glue, focus on business design, config experience as you wish!


Welcome star, PR, and questions! If you find it useful, don't forget to leave a like on crates.io ⭐️!

Commit count: 0

cargo fmt