stakpak-gateway 0.3.88

Stakpak: Your DevOps AI Agent. Generate infrastructure code, debug Kubernetes, configure CI/CD, automate deployments, without giving an LLM the keys to production.
Documentation
use crate::types::{ChannelId, ChatType, PeerId};

#[derive(Debug, Clone, Default)]
pub struct RouterConfig {
    pub dm_scope: DmScope,
    pub bindings: Vec<Binding>,
}

#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub enum DmScope {
    Main,
    PerPeer,
    #[default]
    PerChannelPeer,
}

#[derive(Debug, Clone)]
pub struct Binding {
    pub match_rule: BindingMatch,
    pub routing_key: String,
}

#[derive(Debug, Clone)]
pub struct BindingMatch {
    pub channel: ChannelId,
    pub peer: Option<PeerMatch>,
}

#[derive(Debug, Clone)]
pub struct PeerMatch {
    pub kind: PeerMatchKind,
    pub id: String,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum PeerMatchKind {
    Direct,
    Group,
}

pub fn resolve_routing_key(
    config: &RouterConfig,
    channel: &ChannelId,
    peer_id: &PeerId,
    chat_type: &ChatType,
) -> String {
    if let Some(binding) = config
        .bindings
        .iter()
        .find(|binding| binding_matches_peer(binding, channel, peer_id, chat_type))
    {
        return binding.routing_key.clone();
    }

    if let Some(binding) = config
        .bindings
        .iter()
        .find(|binding| binding_matches_channel(binding, channel))
    {
        return binding.routing_key.clone();
    }

    match chat_type {
        ChatType::Direct => match config.dm_scope {
            DmScope::Main => "main".to_string(),
            DmScope::PerPeer => format!("dm:{}", peer_id.0),
            DmScope::PerChannelPeer => format!("{}:dm:{}", channel.0, peer_id.0),
        },
        ChatType::Group { id } => format!("{}:group:{id}", channel.0),
        ChatType::Thread {
            group_id,
            thread_id,
        } => {
            format!("{}:thread:{group_id}:{thread_id}", channel.0)
        }
    }
}

fn binding_matches_peer(
    binding: &Binding,
    channel: &ChannelId,
    peer_id: &PeerId,
    chat_type: &ChatType,
) -> bool {
    if binding.match_rule.channel != *channel {
        return false;
    }

    let Some(peer_match) = binding.match_rule.peer.as_ref() else {
        return false;
    };

    match peer_match.kind {
        PeerMatchKind::Direct => {
            matches!(chat_type, ChatType::Direct) && peer_match.id == peer_id.0
        }
        PeerMatchKind::Group => match chat_type {
            ChatType::Group { id } => peer_match.id == *id,
            ChatType::Thread { group_id, .. } => peer_match.id == *group_id,
            ChatType::Direct => false,
        },
    }
}

fn binding_matches_channel(binding: &Binding, channel: &ChannelId) -> bool {
    binding.match_rule.channel == *channel && binding.match_rule.peer.is_none()
}

#[cfg(test)]
mod tests {
    use super::{
        Binding, BindingMatch, DmScope, PeerMatch, PeerMatchKind, RouterConfig, resolve_routing_key,
    };
    use crate::types::{ChannelId, ChatType, PeerId};

    #[test]
    fn default_dm_scope_is_per_channel_peer() {
        let config = RouterConfig::default();
        assert_eq!(config.dm_scope, DmScope::PerChannelPeer);
    }

    #[test]
    fn resolves_direct_main_scope() {
        let config = RouterConfig {
            dm_scope: DmScope::Main,
            bindings: Vec::new(),
        };

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Direct,
        );

        assert_eq!(key, "main");
    }

    #[test]
    fn resolves_direct_per_peer_scope() {
        let config = RouterConfig {
            dm_scope: DmScope::PerPeer,
            bindings: Vec::new(),
        };

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Direct,
        );

        assert_eq!(key, "dm:123");
    }

    #[test]
    fn resolves_direct_per_channel_peer_scope() {
        let config = RouterConfig::default();

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Direct,
        );

        assert_eq!(key, "telegram:dm:123");
    }

    #[test]
    fn resolves_group_key() {
        let config = RouterConfig::default();

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Group {
                id: "-100999".to_string(),
            },
        );

        assert_eq!(key, "telegram:group:-100999");
    }

    #[test]
    fn resolves_thread_key() {
        let config = RouterConfig::default();

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Thread {
                group_id: "-100999".to_string(),
                thread_id: "42".to_string(),
            },
        );

        assert_eq!(key, "telegram:thread:-100999:42");
    }

    #[test]
    fn peer_binding_overrides_channel_binding_and_default() {
        let config = RouterConfig {
            dm_scope: DmScope::PerChannelPeer,
            bindings: vec![
                Binding {
                    match_rule: BindingMatch {
                        channel: ChannelId::from("telegram"),
                        peer: Some(PeerMatch {
                            kind: PeerMatchKind::Direct,
                            id: "123".to_string(),
                        }),
                    },
                    routing_key: "peer-bound".to_string(),
                },
                Binding {
                    match_rule: BindingMatch {
                        channel: ChannelId::from("telegram"),
                        peer: None,
                    },
                    routing_key: "channel-bound".to_string(),
                },
            ],
        };

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Direct,
        );

        assert_eq!(key, "peer-bound");
    }

    #[test]
    fn channel_binding_overrides_default_when_no_peer_match() {
        let config = RouterConfig {
            dm_scope: DmScope::PerChannelPeer,
            bindings: vec![Binding {
                match_rule: BindingMatch {
                    channel: ChannelId::from("telegram"),
                    peer: None,
                },
                routing_key: "channel-bound".to_string(),
            }],
        };

        let key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Direct,
        );

        assert_eq!(key, "channel-bound");
    }

    #[test]
    fn group_peer_binding_matches_group_and_thread_parent() {
        let config = RouterConfig {
            dm_scope: DmScope::PerChannelPeer,
            bindings: vec![Binding {
                match_rule: BindingMatch {
                    channel: ChannelId::from("telegram"),
                    peer: Some(PeerMatch {
                        kind: PeerMatchKind::Group,
                        id: "-100999".to_string(),
                    }),
                },
                routing_key: "group-bound".to_string(),
            }],
        };

        let group_key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Group {
                id: "-100999".to_string(),
            },
        );

        let thread_key = resolve_routing_key(
            &config,
            &ChannelId::from("telegram"),
            &PeerId::from("123"),
            &ChatType::Thread {
                group_id: "-100999".to_string(),
                thread_id: "44".to_string(),
            },
        );

        assert_eq!(group_key, "group-bound");
        assert_eq!(thread_key, "group-bound");
    }
}