rithmic_rs/util/
unknown_message.rs1use prost::bytes::Bytes;
4use std::fmt;
5
6const MAX_RENDERED_BYTES: usize = 32;
9
10#[derive(Clone, PartialEq, Eq)]
56#[non_exhaustive]
57pub struct UnknownTemplateMessage {
58 pub template_id: i32,
60 pub payload: Bytes,
65}
66
67impl UnknownTemplateMessage {
68 pub fn new(template_id: i32, payload: Bytes) -> Self {
70 Self {
71 template_id,
72 payload,
73 }
74 }
75
76 pub fn decode_as<M: prost::Message + Default>(&self) -> Result<M, prost::DecodeError> {
91 M::decode(self.payload.clone())
92 }
93
94 pub fn payload_hex(&self) -> String {
99 use fmt::Write;
100
101 let mut hex = String::with_capacity(self.payload.len() * 2);
102
103 for byte in &self.payload {
104 let _ = write!(hex, "{byte:02x}");
106 }
107
108 hex
109 }
110
111 pub fn from_payload_hex(template_id: i32, hex: &str) -> Option<Self> {
117 let hex = hex.trim();
118 let hex = hex
119 .strip_prefix("0x")
120 .or(hex.strip_prefix("0X"))
121 .unwrap_or(hex);
122
123 let digits: Vec<u8> = hex
124 .chars()
125 .filter(|character| !character.is_whitespace())
126 .map(|character| character.to_digit(16).map(|digit| digit as u8))
127 .collect::<Option<_>>()?;
128
129 if digits.len() % 2 != 0 {
130 return None;
131 }
132
133 let payload: Vec<u8> = digits
134 .chunks_exact(2)
135 .map(|pair| (pair[0] << 4) | pair[1])
136 .collect();
137
138 Some(Self::new(template_id, Bytes::from(payload)))
139 }
140}
141
142impl fmt::Display for UnknownTemplateMessage {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(
145 f,
146 "template_id={} ({} bytes)",
147 self.template_id,
148 self.payload.len()
149 )?;
150
151 if self.payload.is_empty() {
152 return Ok(());
153 }
154
155 write!(f, " {}", Hex(&self.payload))
156 }
157}
158
159impl fmt::Debug for UnknownTemplateMessage {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.debug_struct("UnknownTemplateMessage")
163 .field("template_id", &self.template_id)
164 .field("payload_len", &self.payload.len())
165 .field("payload", &format_args!("{}", Hex(&self.payload)))
166 .finish()
167 }
168}
169
170struct Hex<'a>(&'a [u8]);
172
173impl fmt::Display for Hex<'_> {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 for byte in self.0.iter().take(MAX_RENDERED_BYTES) {
176 write!(f, "{byte:02x}")?;
177 }
178
179 let remaining = self.0.len().saturating_sub(MAX_RENDERED_BYTES);
180
181 if remaining > 0 {
182 write!(f, "…+{remaining}B")?;
183 }
184
185 Ok(())
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use prost::Message;
192
193 use super::*;
194 use crate::rti::{RequestCancelAllOrders, RithmicOrderNotification};
195
196 fn frame<M: Message>(template_id: i32, message: &M) -> UnknownTemplateMessage {
197 UnknownTemplateMessage {
198 template_id,
199 payload: Bytes::from(message.encode_to_vec()),
200 }
201 }
202
203 fn notification() -> RithmicOrderNotification {
204 RithmicOrderNotification {
205 template_id: 358,
206 basket_id: Some("9214-2".to_string()),
207 symbol: Some("MESU6".to_string()),
208 price: Some(6412.25),
209 ..RithmicOrderNotification::default()
210 }
211 }
212
213 #[test]
214 fn decodes_into_a_caller_supplied_type() {
215 let original = notification();
216
217 let decoded: RithmicOrderNotification = frame(358, &original)
218 .decode_as()
219 .expect("payload round-trips into the matching type");
220
221 assert_eq!(decoded, original);
222 }
223
224 #[test]
225 fn decode_as_reports_structurally_incompatible_bytes() {
226 let payload = vec![0x9a, 0xb6, 0x4b, 0x02, b'h', b'i'];
230
231 let frame = UnknownTemplateMessage {
232 template_id: 358,
233 payload: Bytes::from(payload),
234 };
235
236 assert!(frame.decode_as::<RequestCancelAllOrders>().is_err());
237 }
238
239 #[test]
240 fn decode_as_can_succeed_against_the_wrong_type() {
241 let decoded = frame(358, ¬ification())
244 .decode_as::<RequestCancelAllOrders>()
245 .expect("unknown fields are skipped, so this decodes");
246
247 assert_eq!(decoded.template_id, 358);
248 assert_eq!(decoded.account_id, None);
249 }
250
251 #[test]
252 fn payload_hex_round_trips_verbatim() {
253 let captured = frame(358, ¬ification());
254 let hex = captured.payload_hex();
255
256 assert_eq!(hex.len(), captured.payload.len() * 2);
258 assert!(hex.chars().all(|character| character.is_ascii_hexdigit()));
259
260 let replayed = UnknownTemplateMessage::from_payload_hex(358, &hex)
261 .expect("payload_hex output parses back");
262
263 assert_eq!(replayed, captured);
264 }
265
266 #[test]
267 fn from_payload_hex_tolerates_copy_paste() {
268 let expected = UnknownTemplateMessage {
269 template_id: 358,
270 payload: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
271 };
272
273 for input in [
274 "deadbeef",
275 "DEADBEEF",
276 "0xdeadbeef",
277 " dead beef\n",
278 "dead\nbeef",
279 ] {
280 assert_eq!(
281 UnknownTemplateMessage::from_payload_hex(358, input).as_ref(),
282 Some(&expected),
283 "{input:?}"
284 );
285 }
286 }
287
288 #[test]
289 fn from_payload_hex_rejects_malformed_input() {
290 assert_eq!(UnknownTemplateMessage::from_payload_hex(358, "abc"), None);
292 assert_eq!(UnknownTemplateMessage::from_payload_hex(358, "zz"), None);
293 }
294
295 #[test]
296 fn display_elides_a_long_payload() {
297 let frame = UnknownTemplateMessage {
298 template_id: 358,
299 payload: Bytes::from(vec![0xab; MAX_RENDERED_BYTES + 20]),
300 };
301
302 let rendered = frame.to_string();
303
304 assert!(
305 rendered.starts_with("template_id=358 (52 bytes) "),
306 "{rendered}"
307 );
308 assert!(rendered.ends_with("…+20B"), "{rendered}");
309 assert_eq!(frame.payload_hex().len(), 104);
310 }
311}