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}