trafix_codec/message/
mod.rs

1//! Implementation of the message module.
2
3pub mod field;
4
5use bytes::Bytes;
6
7use crate::{
8    decoder, encoder,
9    message::field::{
10        Field,
11        value::{begin_string::BeginString, msg_type::MsgType},
12    },
13};
14
15/// Represents the header section of a FIX message.
16///
17/// The header always contains the protocol [`BeginString`] (tag 8)
18/// and the message type [`MsgType`] (tag 35), and may include
19/// additional session or routing fields.
20#[derive(Debug)]
21pub struct Header {
22    /// The `BeginString` identifying the FIX protocol version.
23    #[allow(dead_code)]
24    pub(crate) begin_string: BeginString,
25
26    /// The `MsgType` indicating the business purpose of the message (message type).
27    #[allow(dead_code)]
28    pub(crate) msg_type: MsgType,
29
30    /// Optional additional header fields.
31    pub(crate) fields: Vec<Field>,
32}
33
34/// Represents the body section of a FIX message.
35///
36/// The body always contains the fields forming the message business content.
37#[derive(Default, Debug)]
38pub struct Body {
39    /// Collection of fields forming this message body.
40    pub(crate) fields: Vec<Field>,
41}
42
43/// Represents a complete owned, structured FIX message composed of a header and body.
44///
45/// The header holds protocol and session metadata, while the body
46/// carries message-specific fields defined by the message type.
47#[derive(Debug)]
48pub struct Message {
49    /// The message header containing version, type, and optional routing fields.
50    header: Header,
51
52    /// The message body forming the message business content.
53    body: Body,
54}
55
56impl Message {
57    /// Creates a new [`MessageBuilder`] initialized with the required
58    /// [`BeginString`] and [`MsgType`] header fields.
59    ///
60    /// Example usage:
61    /// ```
62    /// use trafix_codec::message::{
63    ///     Message,
64    ///     field::{
65    ///         Field,
66    ///         value::{begin_string::BeginString, msg_type::MsgType},
67    ///     },
68    /// };
69    ///
70    /// let builder = Message::builder(BeginString::FIX44, MsgType::Logon);
71    /// ```
72    #[must_use]
73    pub fn builder(begin_string: BeginString, msg_type: MsgType) -> MessageBuilder<false> {
74        let header = Header {
75            begin_string,
76            msg_type,
77            fields: Vec::new(),
78        };
79
80        MessageBuilder {
81            inner: Message {
82                header,
83                body: Body::default(),
84            },
85        }
86    }
87
88    /// Encodes this message into a valid, final wire-format `Bytes` buffer, auto populating fields
89    /// `BodyLength` and `Checksum`.
90    #[must_use]
91    pub fn encode(self) -> Bytes {
92        encoder::encode(&self.header, &self.body)
93    }
94
95    /// Decodes a [`Message`] from given bytes. See [`decode`] for more information.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`Error`] on invalid input.
100    pub fn decode(input: impl AsRef<[u8]>) -> Result<Self, decoder::Error> {
101        decoder::decode(input)
102    }
103}
104
105/// Generic builder for constructing [`Message`] instances.
106///
107/// The builder supports chaining calls to add header or body fields.
108/// Type-state (`IS_INIT`) tracks whether at least one body field was added,
109/// allowing [`MessageBuilder::build()`] to only be available after initialization.
110pub struct MessageBuilder<const IS_INIT: bool> {
111    /// The message being constructed.
112    inner: Message,
113}
114
115impl<const IS_INIT: bool> MessageBuilder<IS_INIT> {
116    /// Adds a field to the message header.
117    #[must_use]
118    pub fn with_header(mut self, field: Field) -> Self {
119        self.inner.header.fields.push(field);
120
121        self
122    }
123
124    /// Adds a field to the message body.
125    ///
126    /// Each call appends a new [`Field`] in order of insertion.
127    /// Once at least one field has been added, the builder transitions
128    /// to an initialized state, enabling [`build`](Self::build).
129    #[must_use]
130    pub fn with_field(mut self, field: Field) -> MessageBuilder<true> {
131        self.inner.body.fields.push(field);
132
133        MessageBuilder { inner: self.inner }
134    }
135}
136
137impl MessageBuilder<true> {
138    /// Finalizes and returns the fully constructed [`Message`].
139    ///
140    /// Example usage:
141    /// ```
142    /// use trafix_codec::message::{
143    ///     Message,
144    ///     field::{
145    ///         Field,
146    ///         value::{begin_string::BeginString, msg_type::MsgType},
147    ///     },
148    /// };
149    ///
150    /// let msg = Message::builder(BeginString::FIX44, MsgType::Logout)
151    ///     .with_field(Field::Custom { tag: 58, value: b"Bye".to_vec() })
152    ///     .build();
153    /// ```
154    #[must_use]
155    pub fn build(self) -> Message {
156        self.inner
157    }
158}
159
160#[cfg(test)]
161mod test {
162    use crate::message::{
163        Message,
164        field::{
165            Field,
166            value::{begin_string::BeginString, msg_type::MsgType},
167        },
168    };
169
170    #[test]
171    fn basic_builder() {
172        let builder = Message::builder(BeginString::FIX44, MsgType::Logon);
173
174        // header
175        assert_eq!(builder.inner.header.begin_string, BeginString::FIX44);
176        assert_eq!(builder.inner.header.msg_type, MsgType::Logon);
177
178        // body
179        assert_eq!(builder.inner.body.fields.len(), 0);
180    }
181
182    #[test]
183    fn simple_message() {
184        let builder = Message::builder(BeginString::FIX44, MsgType::Logout);
185
186        let custom_header_field = Field::Custom {
187            tag: 22,
188            value: b"custom_header_field".to_vec(),
189        };
190
191        let custom_body_field1 = Field::Custom {
192            tag: 40000,
193            value: b"custom_body_field1".to_vec(),
194        };
195
196        let custom_body_field2 = Field::Custom {
197            tag: 50000,
198            value: b"custom_body_field2".to_vec(),
199        };
200
201        let msg = builder
202            .with_header(custom_header_field.clone())
203            .with_field(custom_body_field1.clone())
204            .with_field(custom_body_field2.clone())
205            .build();
206
207        // auto-header
208        assert_eq!(msg.header.begin_string, BeginString::FIX44);
209        assert_eq!(msg.header.msg_type, MsgType::Logout);
210
211        // custom header
212        assert_eq!(msg.header.fields.clone().len(), 1);
213        assert_eq!(msg.header.fields[0], custom_header_field);
214
215        // body
216        assert_eq!(msg.body.fields.len(), 2);
217        assert_eq!(msg.body.fields[0], custom_body_field1);
218        assert_eq!(msg.body.fields[1], custom_body_field2);
219    }
220}