binance/spot/
outgoing_message.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2
3use crate::spot::KlineInterval;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum StreamName {
7    /// <symbol>@aggTrade
8    AggTrade { symbol: String },
9    /// <symbol>@trade
10    Trade { symbol: String },
11    /// <symbol>@depth
12    Depth { symbol: String },
13    /// <symbol>@kline_<interval>
14    Kline {
15        symbol: String,
16        interval: KlineInterval,
17    },
18    /// <symbol>@24hrMiniTicker
19    MiniTicker24 { symbol: String },
20}
21
22impl Serialize for StreamName {
23    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
24    where
25        S: Serializer,
26    {
27        let s = match self {
28            StreamName::AggTrade { symbol } => format!("{symbol}@aggTrade"),
29            StreamName::Trade { symbol } => format!("{symbol}@trade"),
30            StreamName::Depth { symbol } => format!("{symbol}@depth"),
31            StreamName::Kline { symbol, interval } => format!("{symbol}@kline_{interval}"),
32            StreamName::MiniTicker24 { symbol } => format!("{symbol}@24hrMiniTicker"),
33        };
34        serializer.serialize_str(&s)
35    }
36}
37
38impl<'de> Deserialize<'de> for StreamName {
39    fn deserialize<D>(deserializer: D) -> Result<StreamName, D::Error>
40    where
41        D: Deserializer<'de>,
42    {
43        let s: &str = Deserialize::deserialize(deserializer)?;
44        if let Some((symbol, kind)) = s.split_once('@') {
45            match kind {
46                "aggTrade" => Ok(StreamName::AggTrade {
47                    symbol: symbol.to_owned(),
48                }),
49                "trade" => Ok(StreamName::Trade {
50                    symbol: symbol.to_owned(),
51                }),
52                "depth" => Ok(StreamName::Depth {
53                    symbol: symbol.to_owned(),
54                }),
55                "24hrMiniTicker" => Ok(StreamName::MiniTicker24 {
56                    symbol: symbol.to_owned(),
57                }),
58                kind => {
59                    if let Some((kind, params)) = kind.split_once('_') {
60                        match kind {
61                            "kline" => {
62                                let interval = format!("\"{params}\"");
63                                let interval = match serde_json::from_str(&interval) {
64                                    Ok(interval) => interval,
65                                    Err(_) => {
66                                        return Err(serde::de::Error::custom(
67                                            "invalid stream format",
68                                        ));
69                                    }
70                                };
71                                Ok(StreamName::Kline {
72                                    symbol: symbol.to_owned(),
73                                    interval,
74                                })
75                            }
76                            _ => Err(serde::de::Error::custom(format!(
77                                "unknown stream type: {}",
78                                kind
79                            ))),
80                        }
81                    } else {
82                        Err(serde::de::Error::custom("invalid stream format"))
83                    }
84                }
85            }
86        } else {
87            Err(serde::de::Error::custom("invalid stream format"))
88        }
89    }
90}
91
92#[derive(Serialize, Debug)]
93#[serde(tag = "method")]
94pub enum OutgoingMessage {
95    Empty,
96    #[serde(rename = "SUBSCRIBE")]
97    Subscribe {
98        id: String,
99        params: Vec<StreamName>,
100    },
101    #[serde(rename = "UNSUBSCRIBE")]
102    Unsubscribe {
103        id: String,
104        params: Vec<StreamName>,
105    },
106    #[serde(rename = "LIST_SUBSCRIPTIONS")]
107    ListSubscriptions {
108        id: String,
109    },
110    #[serde(rename = "SET_PROPERTY")]
111    SetProperty {
112        id: String,
113        params: (String, bool), // ("combined", true | false)
114    },
115    #[serde(rename = "GET_PROPERTY")]
116    GetProperty {
117        id: String,
118        params: String, // "combined"
119    },
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_serialize_stream_name() {
128        let cases = vec![
129            (
130                StreamName::AggTrade {
131                    symbol: String::from("btcusdt"),
132                },
133                r#""btcusdt@aggTrade""#,
134            ),
135            (
136                StreamName::Trade {
137                    symbol: String::from("btcusdt"),
138                },
139                r#""btcusdt@trade""#,
140            ),
141            (
142                StreamName::Depth {
143                    symbol: String::from("btcusdt"),
144                },
145                r#""btcusdt@depth""#,
146            ),
147            (
148                StreamName::Kline {
149                    symbol: String::from("btcusdt"),
150                    interval: KlineInterval::Minute1,
151                },
152                r#""btcusdt@kline_1m""#,
153            ),
154            (
155                StreamName::MiniTicker24 {
156                    symbol: String::from("btcusdt"),
157                },
158                r#""btcusdt@24hrMiniTicker""#,
159            ),
160        ];
161
162        cases.into_iter().for_each(|(stream, expected)| {
163            let serialized = serde_json::to_string(&stream).unwrap();
164            assert_eq!(expected, serialized);
165        });
166    }
167
168    #[test]
169    fn test_deserialize_stream_name() {
170        let cases = vec![
171            (
172                r#""btcusdt@aggTrade""#,
173                StreamName::AggTrade {
174                    symbol: String::from("btcusdt"),
175                },
176            ),
177            (
178                r#""btcusdt@trade""#,
179                StreamName::Trade {
180                    symbol: String::from("btcusdt"),
181                },
182            ),
183            (
184                r#""btcusdt@depth""#,
185                StreamName::Depth {
186                    symbol: String::from("btcusdt"),
187                },
188            ),
189            (
190                r#""btcusdt@kline_1m""#,
191                StreamName::Kline {
192                    symbol: String::from("btcusdt"),
193                    interval: KlineInterval::Minute1,
194                },
195            ),
196            (
197                r#""btcusdt@24hrMiniTicker""#,
198                StreamName::MiniTicker24 {
199                    symbol: String::from("btcusdt"),
200                },
201            ),
202        ];
203
204        cases.into_iter().for_each(|(serialized, expected)| {
205            let stream = serde_json::from_str(serialized).unwrap();
206            assert_eq!(expected, stream);
207        });
208    }
209}