brokapi_bybit_protocol/ws/
models.rs1use serde::Serialize;
8use serde_json::{Value, json};
9
10pub fn public_path(category: &str) -> String {
12 format!("/v5/public/{category}")
13}
14
15pub const PRIVATE_PATH: &str = "/v5/private";
17
18#[derive(Debug, Clone, PartialEq, Serialize)]
20pub struct WsRequest {
21 pub op: String,
23 #[serde(skip_serializing_if = "String::is_empty")]
25 pub req_id: String,
26 #[serde(skip_serializing_if = "Vec::is_empty")]
28 pub args: Vec<Value>,
29}
30
31impl WsRequest {
32 pub fn subscribe(req_id: &str, topic: &str) -> Self {
43 Self {
44 op: "subscribe".into(),
45 req_id: req_id.into(),
46 args: vec![json!(topic)],
47 }
48 }
49
50 pub fn unsubscribe(req_id: &str, topic: &str) -> Self {
52 Self {
53 op: "unsubscribe".into(),
54 req_id: req_id.into(),
55 args: vec![json!(topic)],
56 }
57 }
58
59 pub fn ping() -> Self {
61 Self {
62 op: "ping".into(),
63 req_id: String::new(),
64 args: Vec::new(),
65 }
66 }
67
68 pub fn auth(api_key: &str, api_secret: &str, expires_ms: i64) -> Self {
70 let [key, expires, signature] = crate::sign::ws_auth_args(api_key, api_secret, expires_ms);
71 Self {
72 op: "auth".into(),
73 req_id: String::new(),
74 args: vec![
76 json!(key),
77 json!(expires.parse::<i64>().unwrap_or(expires_ms)),
78 json!(signature),
79 ],
80 }
81 }
82
83 pub fn to_json(&self) -> String {
85 serde_json::to_string(self).unwrap_or_default()
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum DataKind {
92 Snapshot,
94 Delta,
96 Event,
98}
99
100#[derive(Debug, Clone, PartialEq)]
102pub enum WsIncoming {
103 Connected,
106 Ack {
108 op: String,
110 req_id: Option<String>,
112 success: bool,
114 message: String,
116 },
117 Data {
119 topic: String,
121 kind: DataKind,
123 ts: i64,
125 cts: i64,
127 data: Value,
129 },
130 Pong,
132 Unknown(String),
134}
135
136pub fn parse_incoming(text: &str) -> WsIncoming {
152 let Ok(v) = serde_json::from_str::<Value>(text) else {
153 return WsIncoming::Unknown(text.to_string());
154 };
155 let op = v.get("op").and_then(Value::as_str).unwrap_or("");
156 let ret_msg = v.get("ret_msg").and_then(Value::as_str).unwrap_or("");
157 if op == "pong" || ret_msg == "pong" {
158 return WsIncoming::Pong;
159 }
160 if let (Some(topic), Some(data)) = (v.get("topic").and_then(Value::as_str), v.get("data")) {
161 let kind = match v.get("type").and_then(Value::as_str) {
162 Some("snapshot") => DataKind::Snapshot,
163 Some("delta") => DataKind::Delta,
164 _ => DataKind::Event,
165 };
166 let ts = v
167 .get("ts")
168 .or_else(|| v.get("creationTime"))
169 .and_then(Value::as_i64)
170 .unwrap_or(0);
171 return WsIncoming::Data {
172 topic: topic.to_string(),
173 kind,
174 ts,
175 cts: v.get("cts").and_then(Value::as_i64).unwrap_or(0),
176 data: data.clone(),
177 };
178 }
179 if !op.is_empty()
180 && let Some(success) = v.get("success").and_then(Value::as_bool)
181 {
182 return WsIncoming::Ack {
183 op: op.to_string(),
184 req_id: v
185 .get("req_id")
186 .and_then(Value::as_str)
187 .filter(|s| !s.is_empty())
188 .map(str::to_string),
189 success,
190 message: ret_msg.to_string(),
191 };
192 }
193 WsIncoming::Unknown(text.to_string())
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn the_login_carries_the_expires_time_as_a_number() {
202 let r = WsRequest::auth("key", "secret", 1_700_000_000_000);
203 let v: Value = serde_json::from_str(&r.to_json()).unwrap();
204 assert_eq!(v["op"], "auth");
205 assert_eq!(v["args"][0], "key");
206 assert_eq!(v["args"][1], 1_700_000_000_000_i64);
207 assert_eq!(v["args"][2].as_str().unwrap().len(), 64);
208 assert!(v.get("req_id").is_none());
209 }
210
211 #[test]
212 fn a_failed_answer_keeps_the_reason() {
213 let f = parse_incoming(
214 r#"{"success":false,"ret_msg":"error:handler not found,topic:orderbook.7.BTCUSDT","req_id":"b","op":"subscribe"}"#,
215 );
216 match f {
217 WsIncoming::Ack {
218 success,
219 message,
220 req_id,
221 ..
222 } => {
223 assert!(!success);
224 assert!(message.contains("handler not found"));
225 assert_eq!(req_id.as_deref(), Some("b"));
226 }
227 other => panic!("{other:?}"),
228 }
229 }
230
231 #[test]
232 fn a_private_frame_is_an_event_and_a_pong_of_the_public_stream_is_a_pong() {
233 let f = parse_incoming(r#"{"id":"1","topic":"order","creationTime":5,"data":[]}"#);
234 assert!(matches!(
235 f,
236 WsIncoming::Data {
237 kind: DataKind::Event,
238 ts: 5,
239 ..
240 }
241 ));
242 assert_eq!(
243 parse_incoming(
244 r#"{"success":true,"ret_msg":"pong","conn_id":"c","req_id":"","op":"ping"}"#
245 ),
246 WsIncoming::Pong
247 );
248 assert!(matches!(parse_incoming("not json"), WsIncoming::Unknown(_)));
249 }
250}