nautilus_architect_ax/websocket/
parse.rs1use serde::de::Error;
23
24use super::{
25 error::AxWsErrorResponse,
26 messages::{AxMdErrorResponse, AxMdMessage, AxOrdersWsFrame, AxWsOrderResponse},
27};
28
29#[inline]
30fn peek_type_tag(bytes: &[u8]) -> Option<u8> {
31 if bytes.len() > 7
32 && bytes[0] == b'{'
33 && bytes[1] == b'"'
34 && bytes[2] == b't'
35 && bytes[3] == b'"'
36 && bytes[4] == b':'
37 && bytes[5] == b'"'
38 && bytes[7] == b'"'
39 {
40 Some(bytes[6])
41 } else {
42 None
43 }
44}
45
46#[inline]
47fn has_type_tag_prefix(bytes: &[u8]) -> bool {
48 bytes.len() > 5 && bytes[0] == b'{' && bytes[1] == b'"' && bytes[2] == b't' && bytes[3] == b'"'
49}
50
51pub fn parse_md_message(raw: &str) -> Result<AxMdMessage, serde_json::Error> {
61 if let Some(tag) = peek_type_tag(raw.as_bytes()) {
62 return match tag {
63 b'1' => serde_json::from_str(raw).map(AxMdMessage::BookL1),
64 b'2' => serde_json::from_str(raw).map(AxMdMessage::BookL2),
65 b'3' => serde_json::from_str(raw).map(AxMdMessage::BookL3),
66 b's' => serde_json::from_str(raw).map(AxMdMessage::Ticker),
67 b't' => serde_json::from_str(raw).map(AxMdMessage::Trade),
68 b'c' => serde_json::from_str(raw).map(AxMdMessage::Candle),
69 b'h' => serde_json::from_str(raw).map(AxMdMessage::Heartbeat),
70 b'e' => serde_json::from_str::<AxWsErrorResponse>(raw)
71 .map(|resp| AxMdMessage::Error(resp.into())),
72 tag => Err(serde_json::Error::custom(format!(
73 "unknown MD message type tag: '{}'",
74 tag as char
75 ))),
76 };
77 }
78
79 let value: serde_json::Value = serde_json::from_str(raw)?;
81
82 if value.get("result").is_some() {
83 return serde_json::from_value(value).map(AxMdMessage::SubscriptionResponse);
84 }
85
86 if value.get("error").is_some() {
87 return serde_json::from_value::<AxMdErrorResponse>(value)
88 .map(|resp| AxMdMessage::Error(resp.into()));
89 }
90
91 if let Some(t) = value.get("t").and_then(|v| v.as_str()) {
93 match t {
94 "1" => serde_json::from_value(value).map(AxMdMessage::BookL1),
95 "2" => serde_json::from_value(value).map(AxMdMessage::BookL2),
96 "3" => serde_json::from_value(value).map(AxMdMessage::BookL3),
97 "s" => serde_json::from_value(value).map(AxMdMessage::Ticker),
98 "t" => serde_json::from_value(value).map(AxMdMessage::Trade),
99 "c" => serde_json::from_value(value).map(AxMdMessage::Candle),
100 "h" => serde_json::from_value(value).map(AxMdMessage::Heartbeat),
101 "e" => serde_json::from_value::<AxWsErrorResponse>(value)
102 .map(|resp| AxMdMessage::Error(resp.into())),
103 other => Err(serde_json::Error::custom(format!(
104 "unknown MD message type: {other}"
105 ))),
106 }
107 } else {
108 Err(serde_json::Error::custom(
109 "MD message has no 't', 'result', or 'error' field",
110 ))
111 }
112}
113
114pub(crate) fn parse_order_message(raw: &str) -> Result<AxOrdersWsFrame, serde_json::Error> {
121 if has_type_tag_prefix(raw.as_bytes()) {
123 return serde_json::from_str(raw).map(|e| AxOrdersWsFrame::Event(Box::new(e)));
124 }
125
126 let value: serde_json::Value = serde_json::from_str(raw)?;
128
129 if value.get("err").is_some() {
130 return serde_json::from_value(value).map(AxOrdersWsFrame::Error);
131 }
132
133 if let Some(res) = value.get("res") {
134 if res.get("orders").is_some() {
135 return serde_json::from_value(value)
136 .map(|r| AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(r)));
137 }
138
139 if res.get("oid").is_some() {
140 return serde_json::from_value(value)
141 .map(|r| AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(r)));
142 }
143
144 if res.get("cxl_rx").is_some() {
145 return serde_json::from_value(value)
146 .map(|r| AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(r)));
147 }
148
149 if res.get("li").is_some() {
150 return serde_json::from_value(value)
151 .map(|r| AxOrdersWsFrame::Response(AxWsOrderResponse::List(r)));
152 }
153
154 return Err(serde_json::Error::custom(
155 "unrecognized order response shape",
156 ));
157 }
158
159 if value.get("t").is_some() {
161 return serde_json::from_value(value).map(|e| AxOrdersWsFrame::Event(Box::new(e)));
162 }
163
164 Err(serde_json::Error::custom(
165 "order WS message has no 't', 'err', or 'res' field",
166 ))
167}
168
169#[cfg(test)]
170mod tests {
171 use rstest::rstest;
172
173 use super::*;
174 use crate::websocket::messages::{
175 AxMdMessage, AxOrdersWsFrame, AxWsOrderEvent, AxWsOrderResponse,
176 };
177
178 #[rstest]
179 fn test_parse_md_message_unknown_tag_errors() {
180 let raw = r#"{"t":"X","s":"EURUSD-PERP"}"#;
181 let err = parse_md_message(raw).expect_err("unknown tag should error");
182 assert!(err.to_string().contains("unknown MD message type tag"));
183 }
184
185 #[rstest]
186 fn test_parse_md_message_slow_path_subscription_response() {
187 let raw = r#"{"rid":1,"result":{"subscribed":"EURUSD-PERP"}}"#;
188 let msg = parse_md_message(raw).expect("should parse subscription response");
189 assert!(matches!(msg, AxMdMessage::SubscriptionResponse(_)));
190 }
191
192 #[rstest]
193 fn test_parse_md_message_slow_path_error_response() {
194 let raw = r#"{"rid":2,"error":{"code":400,"message":"bad"}}"#;
195 let msg = parse_md_message(raw).expect("should parse error response");
196 match msg {
197 AxMdMessage::Error(err) => {
198 assert_eq!(err.message, "bad");
199 assert_eq!(err.request_id, Some(2));
200 }
201 other => panic!("expected Error variant, was {other:?}"),
202 }
203 }
204
205 #[rstest]
206 fn test_parse_md_message_no_recognized_fields_errors() {
207 let raw = r#"{"foo":"bar"}"#;
208 let err = parse_md_message(raw).expect_err("should reject unknown shape");
209 assert!(
210 err.to_string()
211 .contains("no 't', 'result', or 'error' field")
212 );
213 }
214
215 #[rstest]
216 fn test_parse_md_message_malformed_json_errors() {
217 let raw = "not json";
218 assert!(parse_md_message(raw).is_err());
219 }
220
221 #[rstest]
222 fn test_parse_order_message_unrecognized_res_errors() {
223 let raw = r#"{"rid":1,"res":{"foo":"bar"}}"#;
224 let err = parse_order_message(raw).expect_err("unrecognized res shape should error");
225 assert!(
226 err.to_string()
227 .contains("unrecognized order response shape")
228 );
229 }
230
231 #[rstest]
232 fn test_parse_order_message_no_recognized_fields_errors() {
233 let raw = r#"{"foo":"bar"}"#;
234 let err = parse_order_message(raw).expect_err("unknown shape should error");
235 assert!(err.to_string().contains("no 't', 'err', or 'res' field"));
236 }
237
238 #[rstest]
239 fn test_parse_order_message_malformed_json_errors() {
240 let raw = "not json";
241 assert!(parse_order_message(raw).is_err());
242 }
243
244 #[rstest]
245 fn test_parse_order_message_list_response_with_orders() {
246 let raw = r#"{"rid":0,"res":{"li":"01KCQM-4WP1-0000","o":[]}}"#;
247 let msg = parse_order_message(raw).expect("should parse list response");
248 assert!(matches!(
249 msg,
250 AxOrdersWsFrame::Response(AxWsOrderResponse::List(_))
251 ));
252 }
253
254 #[rstest]
255 fn test_parse_order_message_replaced_live_shape() {
256 let raw = include_str!("../../test_data/ws_order_replaced_live.json");
257 let msg = parse_order_message(raw).expect("should parse live replaced event");
258
259 let AxOrdersWsFrame::Event(event) = msg else {
260 panic!("expected Event frame");
261 };
262 let AxWsOrderEvent::Replaced(replaced) = *event else {
263 panic!("expected Replaced event");
264 };
265
266 assert_eq!(replaced.no.oid, "O-01KWY01WX8JT4DABKC6FRS5NT4");
267 assert_eq!(replaced.no.rq, 100);
268 }
269
270 #[rstest]
271 #[case::direct_open_orders_array(
272 include_str!("../../test_data/ws_order_open_orders_response_invalid_direct_array.json"),
273 "unrecognized order response shape",
274 )]
275 #[case::single_replacement_order(
276 include_str!("../../test_data/ws_order_replaced_invalid_single_order.json"),
277 "missing field `ro`",
278 )]
279 fn test_parse_order_message_rejects_obsolete_shapes(
280 #[case] raw: &str,
281 #[case] expected_error: &str,
282 ) {
283 let error = parse_order_message(raw).expect_err("obsolete shape should be rejected");
284 assert!(
285 error.to_string().contains(expected_error),
286 "expected {expected_error:?} in {error}",
287 );
288 }
289}