Skip to main content

ironfix_tagvalue/
encoder.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! FIX message encoder.
8//!
9//! This module provides an encoder for building FIX messages in the
10//! standard tag=value format.
11
12use crate::checksum::{calculate_checksum, format_checksum};
13use bytes::{BufMut, BytesMut};
14
15/// SOH (Start of Header) delimiter used in FIX messages.
16pub const SOH: u8 = 0x01;
17
18/// FIX message encoder.
19///
20/// The encoder builds FIX messages by appending fields in tag=value format.
21/// It handles BeginString, BodyLength, and Checksum fields automatically.
22#[derive(Debug)]
23pub struct Encoder {
24    /// Buffer for the message body (between BodyLength and Checksum).
25    body: BytesMut,
26    /// The BeginString value (e.g., "FIX.4.4").
27    begin_string: &'static str,
28}
29
30impl Encoder {
31    /// Creates a new encoder with the specified BeginString.
32    ///
33    /// # Arguments
34    /// * `begin_string` - The FIX version string (e.g., "FIX.4.4")
35    #[must_use]
36    pub fn new(begin_string: &'static str) -> Self {
37        Self {
38            body: BytesMut::with_capacity(256),
39            begin_string,
40        }
41    }
42
43    /// Creates a new encoder with pre-allocated capacity.
44    ///
45    /// # Arguments
46    /// * `begin_string` - The FIX version string
47    /// * `capacity` - Initial buffer capacity in bytes
48    #[must_use]
49    pub fn with_capacity(begin_string: &'static str, capacity: usize) -> Self {
50        Self {
51            body: BytesMut::with_capacity(capacity),
52            begin_string,
53        }
54    }
55
56    /// Appends a field with a string value.
57    ///
58    /// # Arguments
59    /// * `tag` - The field tag number
60    /// * `value` - The field value
61    #[inline]
62    pub fn put_str(&mut self, tag: u32, value: &str) {
63        self.put_raw(tag, value.as_bytes());
64    }
65
66    /// Appends a field with an integer value.
67    ///
68    /// # Arguments
69    /// * `tag` - The field tag number
70    /// * `value` - The field value
71    #[inline]
72    pub fn put_int(&mut self, tag: u32, value: i64) {
73        let mut buf = itoa::Buffer::new();
74        let s = buf.format(value);
75        self.put_raw(tag, s.as_bytes());
76    }
77
78    /// Appends a field with an unsigned integer value.
79    ///
80    /// # Arguments
81    /// * `tag` - The field tag number
82    /// * `value` - The field value
83    #[inline]
84    pub fn put_uint(&mut self, tag: u32, value: u64) {
85        let mut buf = itoa::Buffer::new();
86        let s = buf.format(value);
87        self.put_raw(tag, s.as_bytes());
88    }
89
90    /// Appends a field with a boolean value (Y/N).
91    ///
92    /// # Arguments
93    /// * `tag` - The field tag number
94    /// * `value` - The field value
95    #[inline]
96    pub fn put_bool(&mut self, tag: u32, value: bool) {
97        self.put_raw(tag, if value { b"Y" } else { b"N" });
98    }
99
100    /// Appends a field with a single character value.
101    ///
102    /// # Arguments
103    /// * `tag` - The field tag number
104    /// * `value` - The field value
105    #[inline]
106    pub fn put_char(&mut self, tag: u32, value: char) {
107        let mut buf = [0u8; 4];
108        let s = value.encode_utf8(&mut buf);
109        self.put_raw(tag, s.as_bytes());
110    }
111
112    /// Appends a field with raw bytes.
113    ///
114    /// # Arguments
115    /// * `tag` - The field tag number
116    /// * `value` - The field value bytes
117    #[inline]
118    pub fn put_raw(&mut self, tag: u32, value: &[u8]) {
119        let mut tag_buf = itoa::Buffer::new();
120        let tag_str = tag_buf.format(tag);
121
122        self.body.put_slice(tag_str.as_bytes());
123        self.body.put_u8(b'=');
124        self.body.put_slice(value);
125        self.body.put_u8(SOH);
126    }
127
128    /// Finalizes the message and returns the complete encoded bytes.
129    ///
130    /// This method:
131    /// 1. Prepends BeginString (tag 8) and BodyLength (tag 9)
132    /// 2. Appends Checksum (tag 10)
133    ///
134    /// # Returns
135    /// The complete FIX message as bytes.
136    #[must_use]
137    pub fn finish(self) -> BytesMut {
138        let body_len = self.body.len();
139
140        // Build header: 8=BeginString|9=BodyLength|
141        let mut header = BytesMut::with_capacity(32);
142        header.put_slice(b"8=");
143        header.put_slice(self.begin_string.as_bytes());
144        header.put_u8(SOH);
145        header.put_slice(b"9=");
146
147        let mut len_buf = itoa::Buffer::new();
148        let len_str = len_buf.format(body_len);
149        header.put_slice(len_str.as_bytes());
150        header.put_u8(SOH);
151
152        // Combine header and body
153        let mut message = BytesMut::with_capacity(header.len() + body_len + 8);
154        message.put_slice(&header);
155        message.put_slice(&self.body);
156
157        // Calculate and append checksum
158        let checksum = calculate_checksum(&message);
159        let checksum_bytes = format_checksum(checksum);
160
161        message.put_slice(b"10=");
162        message.put_slice(&checksum_bytes);
163        message.put_u8(SOH);
164
165        message
166    }
167
168    /// Returns the current body length.
169    #[inline]
170    #[must_use]
171    pub fn body_len(&self) -> usize {
172        self.body.len()
173    }
174
175    /// Clears the encoder for reuse.
176    #[inline]
177    pub fn clear(&mut self) {
178        self.body.clear();
179    }
180}
181
182impl Default for Encoder {
183    fn default() -> Self {
184        Self::new("FIX.4.4")
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_encoder_basic() {
194        let mut encoder = Encoder::new("FIX.4.4");
195        encoder.put_str(35, "0");
196
197        let message = encoder.finish();
198        let msg_str = String::from_utf8_lossy(&message);
199
200        assert!(msg_str.starts_with("8=FIX.4.4\x01"));
201        assert!(msg_str.contains("35=0\x01"));
202        assert!(msg_str.contains("10="));
203    }
204
205    #[test]
206    fn test_encoder_multiple_fields() {
207        let mut encoder = Encoder::new("FIX.4.4");
208        encoder.put_str(35, "D");
209        encoder.put_str(49, "SENDER");
210        encoder.put_str(56, "TARGET");
211        encoder.put_uint(34, 1);
212
213        let message = encoder.finish();
214        let msg_str = String::from_utf8_lossy(&message);
215
216        assert!(msg_str.contains("35=D\x01"));
217        assert!(msg_str.contains("49=SENDER\x01"));
218        assert!(msg_str.contains("56=TARGET\x01"));
219        assert!(msg_str.contains("34=1\x01"));
220    }
221
222    #[test]
223    fn test_encoder_bool() {
224        let mut encoder = Encoder::new("FIX.4.4");
225        encoder.put_bool(141, true);
226        encoder.put_bool(142, false);
227
228        let message = encoder.finish();
229        let msg_str = String::from_utf8_lossy(&message);
230
231        assert!(msg_str.contains("141=Y\x01"));
232        assert!(msg_str.contains("142=N\x01"));
233    }
234
235    #[test]
236    fn test_encoder_char() {
237        let mut encoder = Encoder::new("FIX.4.4");
238        encoder.put_char(54, '1');
239
240        let message = encoder.finish();
241        let msg_str = String::from_utf8_lossy(&message);
242
243        assert!(msg_str.contains("54=1\x01"));
244    }
245
246    #[test]
247    fn test_encoder_clear() {
248        let mut encoder = Encoder::new("FIX.4.4");
249        encoder.put_str(35, "0");
250        assert!(encoder.body_len() > 0);
251
252        encoder.clear();
253        assert_eq!(encoder.body_len(), 0);
254    }
255}