crypto-msg-type 1.0.12

Cryptocurrenty message type
Documentation
use std::collections::HashMap;

use crate::MessageType;

fn get_market_type(symbol: &str) -> &'static str {
    if symbol.ends_with("-SWAP") {
        "swap"
    } else {
        let c = symbol.matches('-').count();
        if c == 1 {
            "spot"
        } else if c == 2 {
            let date = &symbol[(symbol.len() - 6)..];
            debug_assert!(date.parse::<i64>().is_ok());
            "futures"
        } else {
            debug_assert!(symbol.ends_with("-C") || symbol.ends_with("-P"));
            "option"
        }
    }
}

fn msg_type_to_channel(msg_type: MessageType) -> &'static str {
    match msg_type {
        MessageType::Trade => "trade",
        MessageType::L2Event => "depth_l2_tbt",
        MessageType::L2TopK => "depth5",
        MessageType::BBO => "ticker",
        MessageType::Ticker => "ticker",
        MessageType::Candlestick => "candle",
        _ => panic!("Unknown message type {msg_type}"),
    }
}

fn channel_symbol_to_topic(
    channel: &str,
    symbol: &str,
    configs: Option<&HashMap<String, String>>,
) -> String {
    let market_type = get_market_type(symbol);
    if channel == "candle" {
        format!("{}/candle{}s:{}", market_type, configs.unwrap().get("interval").unwrap(), symbol)
    } else {
        format!("{market_type}/{channel}:{symbol}")
    }
}

fn topics_to_command(topics: &[String], subscribe: bool) -> String {
    format!(
        r#"{{"op":"{}","args":{}}}"#,
        if subscribe { "subscribe" } else { "unsubscribe" },
        serde_json::to_string(topics).unwrap()
    )
}

pub(crate) fn get_ws_commands(
    msg_types: &[MessageType],
    symbols: &[String],
    subscribe: bool,
    configs: Option<&HashMap<String, String>>,
) -> Vec<String> {
    let topics = msg_types
        .iter()
        .map(|msg_type| msg_type_to_channel(*msg_type))
        .flat_map(|channel| {
            symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs))
        })
        .collect::<Vec<String>>();
    vec![topics_to_command(&topics, subscribe)]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn single_msg_type_multiple_symbols() {
        let commands = get_ws_commands(
            &[MessageType::Trade],
            &["BTC-USDT-SWAP".to_string(), "ETH-USDT-SWAP".to_string()],
            true,
            None,
        );
        assert_eq!(commands.len(), 1);
        assert_eq!(
            r#"{"op":"subscribe","args":["swap/trade:BTC-USDT-SWAP","swap/trade:ETH-USDT-SWAP"]}"#,
            commands[0]
        );
    }

    #[test]
    fn multiple_msg_types_single_symbol() {
        let commands = get_ws_commands(
            &[MessageType::Trade, MessageType::L2Event],
            &["BTC-USDT-SWAP".to_string()],
            true,
            None,
        );
        assert_eq!(commands.len(), 1);
        assert_eq!(
            r#"{"op":"subscribe","args":["swap/trade:BTC-USDT-SWAP","swap/depth_l2_tbt:BTC-USDT-SWAP"]}"#,
            commands[0]
        );
    }

    #[test]
    fn candlestick() {
        let mut configs = HashMap::new();
        configs.insert("interval".to_string(), "60".to_string());
        let commands = get_ws_commands(
            &[MessageType::Candlestick],
            &["BTC-USDT-SWAP".to_string(), "ETH-USDT-SWAP".to_string()],
            true,
            Some(&configs),
        );
        assert_eq!(commands.len(), 1);
        assert_eq!(
            r#"{"op":"subscribe","args":["swap/candle60s:BTC-USDT-SWAP","swap/candle60s:ETH-USDT-SWAP"]}"#,
            commands[0]
        );
    }
}