trafix_codec/message/field/
mod.rs

1//! Implementation of the field module.
2
3pub mod value;
4
5use crate::message::field::value::aliases::{MsgSeqNum, SenderCompID, SendingTime, TargetCompID};
6
7/// Macro that generates the [`Field`] enum and its core utility methods.
8///
9/// Each macro entry defines:
10/// - the enum variant name,
11/// - the Rust type for its value,
12/// - the FIX tag number,
13/// - a match binding + expression returning the serialized value.
14///
15/// The macro expands into:
16/// - the [`Field`] enum,
17/// - a [`Field::tag`] method returning the tag number,
18/// - a [`Field::value`] method returning the encoded byte value,
19/// - and a [`Field::encode`] method producing the `"tag=value"` byte sequence.
20macro_rules! fields_macro {
21    ($($(#[$($attrs:tt)*])* $variant:ident($type:ty) = $tag:literal => $match:ident $expr:expr),+) => {
22        /// Represents a single FIX field.
23        ///
24        /// Each variant corresponds to a strongly-typed FIX tag, such as
25        /// `MsgSeqNum(34)` or `SenderCompID(49)`. Fields not covered by
26        /// predefined variants can be represented using [`Field::Custom`].
27        #[derive(Debug, Clone, PartialEq)]
28        pub enum Field {
29            $(
30            $(#[$($attrs)*])*
31            $variant($type)
32            ),+,
33
34            /// Represents an arbitrary or user-defined FIX field not covered
35            /// by the predefined variants.
36            ///
37            /// Useful for extension tags, firm-specific fields, or when
38            /// working with non-standard message structures.
39            Custom {
40                /// Tag of the custom field.
41                tag: u16,
42                /// Contents of the custom field.
43                value: Vec<u8>
44            }
45        }
46
47        impl Field {
48            /// Tries to construct a new [`Field`] from the given tag and value.
49            ///
50            /// # Errors
51            ///
52            /// This function might return error if invalid values are passed for the given tag.
53            pub fn try_new(tag: u16, bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
54                use value::FromFixBytes;
55
56                match tag {
57                    $(
58                    $tag => Ok(Self::$variant(<$type as FromFixBytes>::from_fix_bytes(bytes)?)),
59                    )*
60                    other => Ok(Field::Custom {
61                        tag: other,
62                        value: bytes.into(),
63                    })
64                }
65            }
66
67            /// Returns the numeric FIX tag associated with this field.
68            ///
69            /// Example usage:
70            /// ```
71            /// use trafix_codec::message::field::{Field, value::aliases::MsgSeqNum};
72            /// let f = Field::MsgSeqNum(1);
73            /// assert_eq!(f.tag(), 34);
74            /// ```
75            #[must_use]
76            pub fn tag(&self) -> u16 {
77                match self {
78                    $(
79                    Field::$variant(_) => $tag
80                    ),+,
81
82                    Field::Custom { tag, .. } => { *tag }
83                }
84            }
85
86            /// Returns the serialized value of the field as raw bytes.
87            ///
88            /// For predefined fields, this returns their encoded textual
89            /// representation (e.g. integer → ASCII). For custom fields, the
90            /// original byte vector is cloned.
91            #[must_use]
92            pub fn value(&self) -> Vec<u8> {
93                match self {
94                    $(
95                    Field::$variant($match) => $expr
96                    ),+,
97
98                    Field::Custom { value, .. } => { value.clone() }
99                }
100            }
101
102            /// Serializes the field into its `"tag=value"` representation.
103            ///
104            /// This does **not** append the SOH delimiter; it only produces
105            /// the byte content for a single field. The encoder is
106            /// responsible for joining fields with SOH (`0x01`).
107            ///
108            /// ```
109            /// use trafix_codec::message::field::{Field, value::aliases::MsgSeqNum};
110            /// let f = Field::MsgSeqNum(4);
111            /// assert_eq!(f.encode(), b"34=4".to_vec());
112            /// ```
113            #[must_use]
114            pub fn encode(&self) -> Vec<u8> {
115                match self {
116                    $(
117                    Field::$variant($match) => {
118                        let tag = $tag;
119                        let mut val = $expr;
120
121                        let mut field = format!("{tag}=").into_bytes();
122                        field.append(&mut val);
123
124                        field
125                    }
126                    ),+,
127
128                    Field::Custom { tag, value } => {
129                        let mut field = format!("{tag}=").into_bytes();
130                        field.append(&mut value.clone());
131
132                        field
133                    }
134                }
135            }
136        }
137    };
138}
139
140fields_macro! {
141    /// Message sequence number (`34`).
142    ///
143    /// Used to identify message ordering within a FIX session.
144    MsgSeqNum(MsgSeqNum) = 34 => msg_seq_num format!("{msg_seq_num}").into_bytes(),
145
146    /// Sender company or system identifier (`49`).
147    ///
148    /// Identifies the sender of the message in a FIX session.
149    SenderCompID(SenderCompID) = 49 => sender_comp_id sender_comp_id.clone(),
150
151    /// Message sending time (`52`).
152    ///
153    /// Timestamp representing when the message was sent.
154    SendingTime(SendingTime) = 52 => sending_time sending_time.clone(),
155
156    /// Target company or system identifier (`56`).
157    ///
158    /// Identifies the intended recipient of the message in a FIX session.
159    TargetCompID(TargetCompID) = 56 => target_comp_id target_comp_id.clone()
160}
161
162#[cfg(test)]
163mod test {
164    use crate::message::field::{
165        Field,
166        value::aliases::{MsgSeqNum, SenderCompID, SendingTime, TargetCompID},
167    };
168
169    #[test]
170    fn tag() {
171        let msg_seq_num_field = Field::MsgSeqNum(0);
172        assert_eq!(msg_seq_num_field.tag(), 34);
173
174        let sender_comp_id_field = Field::SenderCompID(SenderCompID::new());
175        assert_eq!(sender_comp_id_field.tag(), 49);
176
177        let sending_time_field = Field::SendingTime(SendingTime::new());
178        assert_eq!(sending_time_field.tag(), 52);
179
180        let target_comp_id_field = Field::TargetCompID(TargetCompID::new());
181        assert_eq!(target_comp_id_field.tag(), 56);
182    }
183
184    #[test]
185    fn value() {
186        let target_comp_id = TargetCompID::from(b"trafix-codec");
187        let target_comp_id_field = Field::TargetCompID(target_comp_id.clone());
188
189        assert_eq!(target_comp_id_field.tag(), 56);
190        assert_eq!(target_comp_id_field.value(), target_comp_id);
191    }
192
193    #[test]
194    fn encode() {
195        let msg_seq_num: MsgSeqNum = 4;
196        let msg_seq_num_field = Field::MsgSeqNum(msg_seq_num);
197
198        assert_eq!(msg_seq_num_field.tag(), 34);
199        assert_eq!(
200            msg_seq_num_field.value(),
201            format!("{msg_seq_num}").into_bytes()
202        );
203
204        // b"34=4"
205        assert_eq!(
206            msg_seq_num_field.encode(),
207            format!("34={msg_seq_num}").into_bytes()
208        );
209    }
210
211    #[test]
212    fn custom_field() {
213        let tag = 62000;
214        let value = b"trafix-codec".to_vec();
215
216        let custom_field = Field::Custom {
217            tag,
218            value: value.clone(),
219        };
220
221        assert_eq!(custom_field.tag(), tag);
222        assert_eq!(custom_field.value(), value.clone());
223
224        let mut encoded = Vec::from(tag.to_string().as_bytes());
225        encoded.extend(b"=");
226        encoded.extend(value);
227
228        // b"62000=trafix-codec"
229        assert_eq!(custom_field.encode(), encoded);
230    }
231}