ironfix_engine/outbound.rs
1/******************************************************************************
2 Author: Joaquín Béjar García
3 Email: jb@taunais.com
4 Date: 14/7/26
5******************************************************************************/
6
7//! Outbound application message builder.
8
9use ironfix_core::message::MsgType;
10
11/// An outbound application message: a MsgType plus ordered body fields.
12///
13/// The engine stamps the standard header (BeginString, BodyLength, MsgType,
14/// SenderCompID, TargetCompID, MsgSeqNum, SendingTime) and the trailer when
15/// the message is sent, so the builder only carries body fields. Fields are
16/// encoded in insertion order.
17#[derive(Debug, Clone)]
18pub struct OutboundMessage {
19 /// Message type (tag 35).
20 msg_type: MsgType,
21 /// Body fields in insertion order.
22 fields: Vec<(u32, Vec<u8>)>,
23}
24
25impl OutboundMessage {
26 /// Creates a new outbound message of the given type.
27 ///
28 /// # Arguments
29 /// * `msg_type` - The message type (tag 35)
30 #[must_use]
31 pub fn new(msg_type: MsgType) -> Self {
32 Self {
33 msg_type,
34 fields: Vec::new(),
35 }
36 }
37
38 /// Returns the message type.
39 #[must_use]
40 pub fn msg_type(&self) -> &MsgType {
41 &self.msg_type
42 }
43
44 /// Appends a field with raw bytes.
45 ///
46 /// # Arguments
47 /// * `tag` - The field tag number
48 /// * `value` - The field value bytes
49 pub fn push_raw(&mut self, tag: u32, value: impl Into<Vec<u8>>) -> &mut Self {
50 self.fields.push((tag, value.into()));
51 self
52 }
53
54 /// Appends a field with a string value.
55 ///
56 /// # Arguments
57 /// * `tag` - The field tag number
58 /// * `value` - The field value
59 pub fn push_str(&mut self, tag: u32, value: &str) -> &mut Self {
60 self.push_raw(tag, value.as_bytes().to_vec())
61 }
62
63 /// Appends a field with an integer value.
64 ///
65 /// # Arguments
66 /// * `tag` - The field tag number
67 /// * `value` - The field value
68 pub fn push_int(&mut self, tag: u32, value: i64) -> &mut Self {
69 self.push_raw(tag, value.to_string().into_bytes())
70 }
71
72 /// Appends a field with an unsigned integer value.
73 ///
74 /// # Arguments
75 /// * `tag` - The field tag number
76 /// * `value` - The field value
77 pub fn push_uint(&mut self, tag: u32, value: u64) -> &mut Self {
78 self.push_raw(tag, value.to_string().into_bytes())
79 }
80
81 /// Appends a field with a single character value.
82 ///
83 /// # Arguments
84 /// * `tag` - The field tag number
85 /// * `value` - The field value
86 pub fn push_char(&mut self, tag: u32, value: char) -> &mut Self {
87 let mut buf = [0u8; 4];
88 let s = value.encode_utf8(&mut buf);
89 self.push_raw(tag, s.as_bytes().to_vec())
90 }
91
92 /// Appends a field with a boolean value (Y/N).
93 ///
94 /// # Arguments
95 /// * `tag` - The field tag number
96 /// * `value` - The field value
97 pub fn push_bool(&mut self, tag: u32, value: bool) -> &mut Self {
98 self.push_raw(tag, if value { b"Y".to_vec() } else { b"N".to_vec() })
99 }
100
101 /// Returns the body fields in insertion order.
102 #[must_use]
103 pub fn fields(&self) -> &[(u32, Vec<u8>)] {
104 &self.fields
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_outbound_message_fields_in_order() {
114 let mut msg = OutboundMessage::new(MsgType::NewOrderSingle);
115 msg.push_str(11, "ORDER-1")
116 .push_char(54, '1')
117 .push_uint(38, 100)
118 .push_int(9999, -5)
119 .push_bool(59, true);
120
121 assert_eq!(msg.msg_type(), &MsgType::NewOrderSingle);
122 let fields = msg.fields();
123 assert_eq!(fields[0], (11, b"ORDER-1".to_vec()));
124 assert_eq!(fields[1], (54, b"1".to_vec()));
125 assert_eq!(fields[2], (38, b"100".to_vec()));
126 assert_eq!(fields[3], (9999, b"-5".to_vec()));
127 assert_eq!(fields[4], (59, b"Y".to_vec()));
128 }
129}