Skip to main content

hotfix_message/
builder.rs

1use crate::Part;
2use crate::error::{MessageIntegrityError, ParserError, ParserResult};
3use crate::field_map::Field;
4use crate::field_types::CheckSum;
5use crate::message::{Config, Message};
6use crate::parsed_message::{GarbledReason, InvalidReason, ParsedMessage};
7use crate::parts::{Body, Header, RepeatingGroup, Trailer};
8use crate::tags::{BEGIN_STRING, BODY_LENGTH, CHECK_SUM, MSG_TYPE};
9use hotfix_dictionary::{Dictionary, LayoutItem, LayoutItemKind, TagU32};
10use std::collections::{HashMap, HashSet};
11
12pub const SOH: u8 = 0x1;
13
14/// Length of the checksum field.
15///
16/// It should always be 7 bytes:
17/// - 2 bytes for the tag (`10`)
18/// - a byte for the separator
19/// - 3 bytes for the value
20/// - a byte for the final delimiter
21///
22/// e.g. `10=643|`
23const CHECKSUM_LENGTH: usize = 7;
24
25pub struct MessageBuilder {
26    dict: Dictionary,
27    header_tags: HashSet<TagU32>,
28    trailer_tags: HashSet<TagU32>,
29    message_specification: HashMap<String, MessageSpecification>,
30    config: Config,
31}
32
33impl MessageBuilder {
34    pub fn new(dict: Dictionary, config: Config) -> ParserResult<Self> {
35        let header_tags = Self::get_tags_for_component(&dict, "StandardHeader")?;
36        let trailer_tags = Self::get_tags_for_component(&dict, "StandardTrailer")?;
37        let message_definitions = build_message_specifications(&dict)?;
38
39        let parser = Self {
40            dict,
41            header_tags,
42            trailer_tags,
43            message_specification: message_definitions,
44            config,
45        };
46
47        Ok(parser)
48    }
49
50    pub fn build(&self, data: &[u8]) -> ParsedMessage {
51        let mut parser = Parser {
52            position: 0,
53            raw_data: data,
54            config: &self.config,
55        };
56        let (mut header, mut trailer) = match self.verify_integrity(&mut parser) {
57            Ok((header, trailer)) => (header, trailer),
58            Err(err) => return err.into(),
59        };
60
61        let next = match self.build_header(&mut header, &mut parser) {
62            Ok(next_field) => next_field,
63            Err(err) => {
64                return parser_error_to_parsed_message(err, header);
65            }
66        };
67
68        #[allow(clippy::expect_used)]
69        let msg_type = header.get::<&str>(MSG_TYPE).expect("we know this is valid at this point as we have already verified the integrity of the header");
70        let (body, next) = match self.build_body(msg_type, &mut parser, next) {
71            Ok((body, field)) => (body, field),
72            Err(err) => {
73                return parser_error_to_parsed_message(err, header);
74            }
75        };
76
77        self.build_trailer(&mut trailer, &mut parser, next);
78
79        let msg = Message {
80            header,
81            body,
82            trailer,
83        };
84
85        ParsedMessage::Valid(msg)
86    }
87
88    fn verify_integrity(
89        &self,
90        parser: &mut Parser,
91    ) -> Result<(Header, Trailer), MessageIntegrityError> {
92        let mut header = Header::default();
93
94        // The first field should always be BeginString
95        let begin_string_field = self.parse_begin_string(parser)?;
96        header.fields.insert(begin_string_field);
97
98        // The second field should always be BodyLength
99        let body_length_field = self.parse_body_length(parser)?;
100        header.fields.insert(body_length_field);
101
102        // The BodyLength is the number of bytes between the end of the BodyLength field and the start of the last field (i.e. the checksum)
103        let body_length = if let Ok(body_length) = header.get::<usize>(BODY_LENGTH) {
104            let expected_length = parser.position + body_length + CHECKSUM_LENGTH;
105            if parser.raw_data.len() != expected_length {
106                return Err(MessageIntegrityError::InvalidBodyLength);
107            }
108            body_length
109        } else {
110            // we failed to parse body length as usize
111            return Err(MessageIntegrityError::InvalidBodyLength);
112        };
113
114        // Parse the checksum (at the end of the message) and verify it matches the computed checksum
115        let mut trailer = Trailer::default();
116        let checksum_field = parser.parse_checksum(parser.position + body_length)?;
117        trailer.fields.insert(checksum_field);
118
119        if let Ok(checksum) = trailer.get::<CheckSum>(CHECK_SUM) {
120            let computed_checksum =
121                CheckSum::compute(&parser.raw_data[0..parser.position + body_length]);
122            if computed_checksum != checksum {
123                return Err(MessageIntegrityError::InvalidCheckSum);
124            }
125        }
126
127        // The third field should be the MsgType
128        let msg_type_field = self.parse_message_type(parser)?;
129        header.fields.insert(msg_type_field);
130
131        Ok((header, trailer))
132    }
133
134    fn parse_begin_string(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
135        if let Some(begin_string) = parser.next_field()
136            && begin_string.tag.get() == BEGIN_STRING.tag
137        {
138            Ok(begin_string)
139        } else {
140            Err(MessageIntegrityError::InvalidBeginString)
141        }
142    }
143
144    fn parse_body_length(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
145        if let Some(body_length) = parser.next_field()
146            && body_length.tag.get() == BODY_LENGTH.tag
147        {
148            Ok(body_length)
149        } else {
150            Err(MessageIntegrityError::InvalidBodyLength)
151        }
152    }
153
154    fn parse_message_type(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
155        if let Some(msg_type) = parser.next_field()
156            && msg_type.tag.get() == MSG_TYPE.tag
157        {
158            Ok(msg_type)
159        } else {
160            Err(MessageIntegrityError::InvalidMsgType)
161        }
162    }
163
164    fn build_header(&self, header: &mut Header, parser: &mut Parser) -> ParserResult<Field> {
165        // we have already added the first 3 mandatory fields, build the rest
166
167        loop {
168            let field = parser.next_field().ok_or(ParserError::Malformed(
169                "message ended within header".to_string(),
170            ))?;
171
172            if self.is_header_tag(field.tag) {
173                header.fields.insert(field);
174            } else {
175                // check the message type once all other header fields have been parsed
176                // we delay it until after parsing so our rejection has access to fields like the sequence number
177                #[allow(clippy::expect_used)]
178                let msg_type = header
179                    .get::<&str>(MSG_TYPE)
180                    .expect("this never fails as we've verified the integrity of the header");
181                if self.dict.message_by_msgtype(msg_type).is_none() {
182                    return Err(ParserError::InvalidMsgType(msg_type.to_string()));
183                }
184
185                return Ok(field);
186            }
187        }
188    }
189
190    fn build_body(
191        &self,
192        msg_type: &str,
193        parser: &mut Parser,
194        next_field: Field,
195    ) -> ParserResult<(Body, Field)> {
196        let message_def = self.get_message_def(msg_type)?;
197        let mut body = Body::default();
198        let mut field = next_field;
199
200        while message_def.contains_tag(field.tag) {
201            let tag = field.tag;
202            body.store_field(field);
203
204            // check if it's the start of a group and parse the group as needed
205            let field_def = self.get_dict_field_by_tag(tag.get())?;
206            match message_def.get_group(tag) {
207                Some(group_def) => {
208                    let (groups, next) = Self::parse_groups(parser, group_def, field_def.tag())?;
209                    #[allow(clippy::expect_used)]
210                    body.set_groups(groups)
211                        .expect("groups are guaranteed to be valid at this point");
212                    field = next;
213                }
214                None => {
215                    field = parser.next_field().ok_or(ParserError::Malformed(
216                        "message ended within the body".to_string(),
217                    ))?;
218                }
219            }
220        }
221
222        if !self.is_trailer_tag(field.tag) {
223            return Err(ParserError::InvalidField(field.tag.get()));
224        }
225
226        Ok((body, field))
227    }
228
229    fn build_trailer(&self, trailer: &mut Trailer, parser: &mut Parser, next_field: Field) {
230        let mut field = Some(next_field);
231        while let Some(f) = field {
232            if f.tag.get() == CHECK_SUM.tag {
233                break;
234            }
235            trailer.store_field(f);
236            field = parser.next_field();
237        }
238    }
239
240    fn parse_groups(
241        parser: &mut Parser,
242        group_def: &GroupSpecification,
243        start_tag: TagU32,
244    ) -> ParserResult<(Vec<RepeatingGroup>, Field)> {
245        let mut groups = vec![];
246
247        let mut field = parser.next_field().ok_or(ParserError::Malformed(
248            "missing delimiter field".to_string(),
249        ))?;
250        loop {
251            let mut group = RepeatingGroup::new_with_tags(start_tag, group_def.delimiter_tag());
252
253            // we skip the first field as we've already stored the delimiter
254            for field_def in group_def.fields().iter() {
255                let is_required =
256                    field_def.is_required || field_def.tag == group_def.delimiter_tag();
257                let current_tag = field.tag;
258                if field_def.tag == current_tag {
259                    // the next tag is the next expected field's tag in the group, store it and move on
260                    group.store_field(field);
261                    field = if let Some(nested_group_def) = group_def.get_nested_group(current_tag)
262                    {
263                        let (groups, next) =
264                            Self::parse_groups(parser, nested_group_def, current_tag)?;
265                        #[allow(clippy::expect_used)]
266                        group
267                            .set_groups(groups)
268                            .expect("groups are guaranteed to be valid at this point");
269                        next
270                    } else {
271                        parser
272                            .next_field()
273                            .ok_or(ParserError::Malformed("incomplete group".to_string()))?
274                    }
275                } else if !is_required {
276                    // this field isn't required in the group, so it's fine to skip it
277                } else {
278                    // the next field in the group is required but the next field in the message isn't it
279                    let err = if group_def.contains_tag(field.tag) {
280                        ParserError::InvalidGroupFieldOrder {
281                            tag: field.tag.get(),
282                            group_tag: group_def.number_of_entries_tag().get(),
283                        }
284                    } else {
285                        ParserError::InvalidField(field.tag.get())
286                    };
287                    return Err(err);
288                }
289            }
290
291            // we've checked all fields for this group,
292            // it's either another group in the repeating group or the end of the repeating group
293            groups.push(group);
294
295            if !group_def.contains_tag(field.tag) {
296                return Ok((groups, field));
297            }
298        }
299    }
300
301    fn get_dict_field_by_tag(&self, tag: u32) -> ParserResult<hotfix_dictionary::Field<'_>> {
302        self.dict
303            .field_by_tag(tag)
304            .ok_or(ParserError::InvalidField(tag))
305    }
306
307    fn is_header_tag(&self, tag: TagU32) -> bool {
308        self.header_tags.contains(&tag)
309    }
310
311    fn is_trailer_tag(&self, tag: TagU32) -> bool {
312        self.trailer_tags.contains(&tag)
313    }
314
315    fn get_message_def(&self, msg_type: &str) -> ParserResult<&MessageSpecification> {
316        match self.message_specification.get(msg_type) {
317            Some(message_def) => Ok(message_def),
318            None => Err(ParserError::InvalidMsgType(msg_type.to_string())),
319        }
320    }
321
322    fn get_tags_for_component(
323        dict: &Dictionary,
324        component_name: &str,
325    ) -> ParserResult<HashSet<TagU32>> {
326        let mut tags = HashSet::new();
327        let component = dict
328            .component_by_name(component_name)
329            .ok_or_else(|| ParserError::InvalidComponent(component_name.to_string()))?;
330        for item in component.items() {
331            if let LayoutItemKind::Field(field) = item.kind() {
332                tags.insert(field.tag());
333            }
334        }
335
336        Ok(tags)
337    }
338}
339
340struct Parser<'a> {
341    position: usize,
342    raw_data: &'a [u8],
343    config: &'a Config,
344}
345
346impl<'a> Parser<'a> {
347    fn next_field(&mut self) -> Option<Field> {
348        let (field, end_position) = self.parse_field_at(self.position)?;
349        self.position = end_position + 1;
350
351        Some(field)
352    }
353
354    fn parse_field_at(&self, position: usize) -> Option<(Field, usize)> {
355        let mut iter = self.raw_data[position..].iter();
356        let equal_sign_position = position + iter.position(|c| *c == b'=')?;
357        let bytes_until_separator = iter.position(|c| *c == self.config.separator)?;
358        let separator_position = equal_sign_position + bytes_until_separator + 1;
359
360        let tag = tag_from_bytes(&self.raw_data[position..equal_sign_position])?;
361        let data = self.raw_data[equal_sign_position + 1..separator_position].to_vec();
362        let field = Field::new(tag, data);
363
364        Some((field, separator_position))
365    }
366
367    fn parse_checksum(&self, checksum_start: usize) -> Result<Field, MessageIntegrityError> {
368        if let Some((checksum, _)) = self.parse_field_at(checksum_start)
369            && checksum.tag.get() == CHECK_SUM.tag
370        {
371            Ok(checksum)
372        } else {
373            Err(MessageIntegrityError::InvalidCheckSum)
374        }
375    }
376}
377
378fn tag_from_bytes(bytes: &[u8]) -> Option<TagU32> {
379    let mut tag = 0u32;
380    for byte in bytes.iter().copied() {
381        tag = tag * 10 + (byte as u32 - b'0' as u32);
382    }
383
384    TagU32::new(tag)
385}
386
387fn parser_error_to_parsed_message(err: ParserError, header: Header) -> ParsedMessage {
388    match err {
389        ParserError::IOError(_) => ParsedMessage::Garbled(GarbledReason::Malformed),
390        ParserError::InvalidField(tag) => ParsedMessage::Invalid {
391            reason: InvalidReason::InvalidField(tag),
392            message: Message::with_header(header),
393        },
394        ParserError::InvalidGroup(tag) => ParsedMessage::Invalid {
395            reason: InvalidReason::InvalidGroup(tag),
396            message: Message::with_header(header),
397        },
398        ParserError::InvalidGroupFieldOrder { tag, group_tag } => ParsedMessage::Invalid {
399            reason: InvalidReason::InvalidOrderInGroup { tag, group_tag },
400            message: Message::with_header(header),
401        },
402        ParserError::InvalidComponent(tag) => ParsedMessage::Invalid {
403            reason: InvalidReason::InvalidComponent(tag),
404            message: Message::with_header(header),
405        },
406        ParserError::InvalidMsgType(msg_type) => ParsedMessage::Invalid {
407            reason: InvalidReason::InvalidMsgType(msg_type),
408            message: Message::with_header(header),
409        },
410        ParserError::Malformed(_) => ParsedMessage::Garbled(GarbledReason::Malformed),
411    }
412}
413
414struct FieldSpecification {
415    pub(crate) tag: TagU32,
416    pub(crate) is_required: bool,
417}
418
419struct GroupSpecification {
420    number_of_entries_tag: TagU32,
421    fields: Vec<FieldSpecification>,
422    nested_groups: HashMap<TagU32, GroupSpecification>,
423}
424
425impl GroupSpecification {
426    pub fn fields(&self) -> &[FieldSpecification] {
427        self.fields.as_slice()
428    }
429    pub fn number_of_entries_tag(&self) -> TagU32 {
430        self.number_of_entries_tag
431    }
432
433    pub fn delimiter_tag(&self) -> TagU32 {
434        #[allow(clippy::expect_used)]
435        self.fields
436            .first()
437            .expect("groups always have at least one field")
438            .tag
439    }
440
441    pub fn contains_tag(&self, tag: TagU32) -> bool {
442        self.fields.iter().any(|f| f.tag == tag)
443    }
444
445    pub fn get_nested_group(&self, tag: TagU32) -> Option<&GroupSpecification> {
446        self.nested_groups.get(&tag)
447    }
448}
449
450struct MessageSpecification {
451    fields: Vec<FieldSpecification>,
452    groups: HashMap<TagU32, GroupSpecification>,
453}
454
455impl MessageSpecification {
456    pub fn contains_tag(&self, tag: TagU32) -> bool {
457        self.fields.iter().any(|f| f.tag == tag)
458    }
459
460    pub fn get_group(&self, tag: TagU32) -> Option<&GroupSpecification> {
461        self.groups.get(&tag)
462    }
463}
464
465fn build_message_specifications(
466    dict: &Dictionary,
467) -> ParserResult<HashMap<String, MessageSpecification>> {
468    let mut definitions = HashMap::new();
469
470    for message in dict.messages() {
471        let fields = message
472            .layout()
473            .flat_map(|item| extract_fields(dict, item))
474            .flatten()
475            .collect();
476
477        let mut groups = HashMap::new();
478        for item in message.layout() {
479            groups.extend(extract_groups(dict, item)?);
480        }
481
482        let message_def = MessageSpecification { fields, groups };
483        definitions.insert(message.msg_type().to_string(), message_def);
484    }
485
486    Ok(definitions)
487}
488
489fn extract_fields(dict: &Dictionary, item: LayoutItem) -> ParserResult<Vec<FieldSpecification>> {
490    let is_required = item.required();
491    let fields = match item.kind() {
492        LayoutItemKind::Component(c) => {
493            let component = dict
494                .component_by_name(c.name())
495                .ok_or_else(|| ParserError::InvalidComponent(c.name().to_string()))?;
496            component
497                .items()
498                .flat_map(|i| extract_fields(dict, i))
499                .flatten()
500                .collect()
501        }
502        LayoutItemKind::Field(field) => vec![FieldSpecification {
503            tag: field.tag(),
504            is_required,
505        }],
506        LayoutItemKind::Group(field, _) => vec![FieldSpecification {
507            tag: field.tag(),
508            is_required,
509        }],
510    };
511
512    Ok(fields)
513}
514
515fn extract_groups(
516    dict: &Dictionary,
517    item: LayoutItem,
518) -> ParserResult<HashMap<TagU32, GroupSpecification>> {
519    let mut groups = HashMap::new();
520    match item.kind() {
521        LayoutItemKind::Component(c) => {
522            let component = dict
523                .component_by_name(c.name())
524                .ok_or_else(|| ParserError::InvalidComponent(c.name().to_string()))?;
525            for i in component.items() {
526                groups.extend(extract_groups(dict, i)?);
527            }
528        }
529        LayoutItemKind::Group(field, items) => {
530            let mut nested_groups = HashMap::new();
531            for i in items.iter() {
532                nested_groups.extend(extract_groups(dict, i.clone())?);
533            }
534            groups.insert(
535                field.tag(),
536                GroupSpecification {
537                    number_of_entries_tag: field.tag(),
538                    fields: items
539                        .iter()
540                        .flat_map(|i| extract_fields(dict, i.clone()))
541                        .flatten()
542                        .collect(),
543                    nested_groups,
544                },
545            );
546        }
547        _ => {}
548    };
549
550    Ok(groups)
551}
552
553#[cfg(test)]
554mod tests {
555    use crate::builder::MessageBuilder;
556    use crate::field_types::Currency;
557    use crate::message::Config;
558    use crate::parsed_message::{GarbledReason, InvalidReason, ParsedMessage};
559    use crate::{Part, fix44};
560    use hotfix_dictionary::{Dictionary, IsFieldDefinition, TagU32};
561
562    const CONFIG: Config = Config::with_separator(b'|');
563
564    #[test]
565    fn test_specification_top_level_fields() {
566        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
567        let message_def = builder.get_message_def("J").unwrap();
568
569        // check that it contains `Symbol`, a tag from the nested `Instrument` component
570        assert!(message_def.contains_tag(fix44::SYMBOL.tag()));
571
572        // check that it contains `NoOrders`, the starting tag for `OrdAllocGrp`
573        assert!(message_def.contains_tag(fix44::NO_ORDERS.tag()));
574
575        // check that it doesn't contain other tags from the `OrdAllocGroup`
576        assert!(!message_def.contains_tag(fix44::ORDER_QTY.tag()));
577    }
578
579    #[test]
580    fn test_specification_top_level_groups() {
581        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
582        let message_def = builder.get_message_def("J").unwrap();
583
584        // check that it contains the right number of top-level groups
585        // expected 10 groups (7 directly (including `Parties` and `Stipulations`), 2 in `Instrument`, 1 in `InstrumentExtension`,
586        let expected_group_fields = vec![
587            fix44::NO_ORDERS,
588            fix44::NO_ALLOCS,
589            fix44::NO_EXECS,
590            fix44::NO_STIPULATIONS,
591            fix44::NO_PARTY_I_DS,
592            fix44::NO_SECURITY_ALT_ID,
593            fix44::NO_LEGS,
594            fix44::NO_UNDERLYINGS,
595            fix44::NO_EVENTS,
596            fix44::NO_INSTR_ATTRIB,
597        ];
598        assert_eq!(message_def.groups.len(), expected_group_fields.len());
599        for field in expected_group_fields {
600            assert!(
601                message_def
602                    .get_group(TagU32::new(field.tag).unwrap())
603                    .is_some()
604            );
605        }
606
607        // check that nested groups are not included directly
608        assert!(
609            message_def
610                .get_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
611                .is_none()
612        );
613    }
614
615    #[test]
616    fn test_specification_nested_groups() {
617        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
618        let message_def = builder.get_message_def("J").unwrap();
619
620        // Order allocation groups only have one nested group, the parties
621        let order_alloc_group = message_def.get_group(fix44::NO_ORDERS.tag()).unwrap();
622        assert_eq!(order_alloc_group.nested_groups.len(), 1);
623        let nested_parties_2_group = order_alloc_group
624            .get_nested_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
625            .expect("nested parties group to exist");
626
627        // The parties group only has one nested group, the parties subgroup
628        assert_eq!(nested_parties_2_group.nested_groups.len(), 1);
629        let subgroup = nested_parties_2_group
630            .get_nested_group(fix44::NO_NESTED2_PARTY_SUB_I_DS.tag())
631            .expect("parties subgroup to exist");
632        assert!(subgroup.nested_groups.is_empty());
633    }
634
635    #[test]
636    fn test_specification_field_order_in_nested_group() {
637        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
638        let message_def = builder.get_message_def("J").unwrap();
639
640        // get the parties group nested in the order allocation group
641        let order_alloc_group = message_def.get_group(fix44::NO_ORDERS.tag()).unwrap();
642        assert_eq!(order_alloc_group.nested_groups.len(), 1);
643        let nested_parties_2_group = order_alloc_group
644            .get_nested_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
645            .expect("nested parties group to exist");
646
647        let mut fields = nested_parties_2_group.fields.iter();
648        let expected_fields = vec![
649            (fix44::NESTED2_PARTY_ID, false),
650            (fix44::NESTED2_PARTY_ID_SOURCE, false),
651            (fix44::NESTED2_PARTY_ROLE, false),
652            (fix44::NO_NESTED2_PARTY_SUB_I_DS, false),
653        ];
654
655        for (field_definition, is_required) in expected_fields {
656            let next = fields.next().unwrap();
657            assert_eq!(next.tag.get(), field_definition.tag);
658            assert_eq!(next.is_required, is_required);
659        }
660    }
661
662    #[test]
663    fn parse_simple_message() {
664        let raw = b"8=FIX.4.4|9=40|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=093|";
665        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
666
667        let message = builder.build(raw).into_message().unwrap();
668
669        let begin: &str = message.header().get(fix44::BEGIN_STRING).unwrap();
670        assert_eq!(begin, "FIX.4.4");
671
672        let body_length: u32 = message.header().get(fix44::BODY_LENGTH).unwrap();
673        assert_eq!(body_length, 40);
674
675        let message_type: &str = message.header().get(fix44::MSG_TYPE).unwrap();
676        assert_eq!(message_type, "D");
677
678        let currency: &Currency = message.get(fix44::CURRENCY).unwrap();
679        assert_eq!(currency, b"USD");
680
681        let time_in_force: &str = message.get(fix44::TIME_IN_FORCE).unwrap();
682        assert_eq!(time_in_force, "0");
683
684        let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
685        assert_eq!(checksum, "093");
686    }
687
688    #[test]
689    fn repeating_group_entries() {
690        let raw = b"8=FIX.4.4|9=191|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
691        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
692
693        let message = builder.build(raw).into_message().unwrap();
694
695        let begin: &str = message.header().get(fix44::BEGIN_STRING).unwrap();
696        assert_eq!(begin, "FIX.4.4");
697
698        let fee1 = message.get_group(fix44::NO_MISC_FEES, 0).unwrap();
699        let amt: f64 = fee1.get(fix44::MISC_FEE_AMT).unwrap();
700        assert_eq!(amt, 100.0);
701
702        let fee2 = message.get_group(fix44::NO_MISC_FEES, 1).unwrap();
703        let fee_type: &str = fee2.get(fix44::MISC_FEE_TYPE).unwrap();
704        assert_eq!(fee_type, "7");
705
706        let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
707        assert_eq!(checksum, "140");
708    }
709
710    #[test]
711    fn nested_repeating_group_entries() {
712        let raw = b"8=FIX.4.4|9=247|35=8|34=2|49=Broker|52=20231103-09:30:00|56=Client|11=Order12345|17=Exec12345|150=0|39=0|55=APPL|54=1|38=100|32=50|31=150.00|151=50|14=50|6=150.00|453=2|448=PARTYA|447=D|452=1|802=2|523=SUBPARTYA1|803=1|523=SUBPARTYA2|803=2|448=PARTYB|447=D|452=2|10=129|";
713        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
714        let message = builder.build(raw).into_message().unwrap();
715
716        let party_a = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
717        let party_a_0 = party_a
718            .get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0)
719            .unwrap();
720        let sub_id_0: &str = party_a_0.get(fix44::PARTY_SUB_ID).unwrap();
721        assert_eq!(sub_id_0, "SUBPARTYA1");
722
723        let party_b = message.get_group(fix44::NO_PARTY_I_DS, 1).unwrap();
724        let party_b_id: &str = party_b.get(fix44::PARTY_ID).unwrap();
725        assert_eq!(party_b_id, "PARTYB");
726
727        let party_b_role: u32 = party_b.get(fix44::PARTY_ROLE).unwrap();
728        assert_eq!(party_b_role, 2);
729
730        let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
731        assert_eq!(checksum, "129");
732    }
733
734    #[test]
735    fn test_begin_string_not_the_first_tag() {
736        let raw = b"9=40|8=FIX.4.4|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=093|";
737        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
738        let parsed_message = builder.build(raw);
739
740        assert!(matches!(
741            parsed_message,
742            ParsedMessage::Garbled(GarbledReason::InvalidBeginString)
743        ));
744    }
745
746    #[test]
747    fn test_body_length_not_the_second_tag() {
748        let raw = b"8=FIX.4.4|49=SENDER|9=191|35=8|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
749        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
750        let parsed_message = builder.build(raw);
751
752        assert!(matches!(
753            parsed_message,
754            ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
755        ));
756    }
757
758    #[test]
759    fn test_body_length_is_wrong() {
760        let raw = b"8=FIX.4.4|9=192|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
761        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
762        let parsed_message = builder.build(raw);
763
764        assert!(matches!(
765            parsed_message,
766            ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
767        ));
768    }
769
770    #[test]
771    fn test_body_length_exceeds_message_length() {
772        let raw = b"8=FIX.4.4|9=500|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
773        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
774        let parsed_message = builder.build(raw);
775
776        assert!(matches!(
777            parsed_message,
778            ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
779        ));
780    }
781
782    #[test]
783    fn test_msg_type_is_not_the_third_tag() {
784        let raw = b"8=FIX.4.4|9=191|49=SENDER|35=8|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|139=7|10=140|";
785        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
786        let parsed_message = builder.build(raw);
787
788        assert!(matches!(
789            parsed_message,
790            ParsedMessage::Garbled(GarbledReason::InvalidMsgType)
791        ));
792    }
793
794    #[test]
795    fn test_checksum_is_not_the_last_tag() {
796        let raw = b"8=FIX.4.4|9=191|35=8|49=SENDER|56=TARGET|34=123|52=20231103-12:00:00|11=12345|17=ABC123|150=2|39=1|55=XYZ|54=1|38=200|44=10|32=100|31=10|14=100|6=10|151=100|136=2|137=100|138=EUR|139=7|137=160|138=GBP|10=140|139=7|";
797        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
798        let parsed_message = builder.build(raw);
799
800        assert!(matches!(
801            parsed_message,
802            ParsedMessage::Garbled(GarbledReason::InvalidChecksum)
803        ));
804    }
805
806    #[test]
807    fn test_invalid_checksum() {
808        let raw = b"8=FIX.4.4|9=40|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=000|";
809        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
810        let parsed_message = builder.build(raw);
811
812        assert!(matches!(
813            parsed_message,
814            ParsedMessage::Garbled(GarbledReason::InvalidChecksum)
815        ));
816    }
817
818    #[test]
819    fn test_invalid_field_in_body() {
820        let raw = b"8=FIX.4.4|9=53|35=D|49=AFUNDMGR|9999=invalid|56=ABROKER|15=USD|59=0|10=229|";
821        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
822        let parsed_message = builder.build(raw);
823
824        assert!(matches!(
825            parsed_message,
826            ParsedMessage::Invalid {
827                reason: InvalidReason::InvalidField(_),
828                ..
829            }
830        ));
831    }
832
833    #[test]
834    fn test_invalid_group_in_body() {
835        // tag=384 is `NoMsgTypes`, which is supposed to have `RefMsgType` (tag=372) and `MsgDirection` (tag=385)
836        // in our message, `RefMsgType` is missing
837        let raw = b"8=FIX.4.4|9=75|35=A|49=SENDER|56=TARGET|34=1|52=20231103-12:00:00|98=0|108=30|384=1|385=R|10=050|";
838        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
839        let parsed_message = builder.build(raw);
840
841        assert!(matches!(
842            parsed_message,
843            ParsedMessage::Invalid {
844                reason: InvalidReason::InvalidOrderInGroup {
845                    tag: 385,
846                    group_tag: 384
847                },
848                ..
849            }
850        ));
851    }
852
853    #[test]
854    fn test_parsing_nested_component_inside_group() {
855        // an `AllocationInstruction` with `CommissionData` nested inside `AllocGrp`
856        let raw_instrument = "55=AAPL|107=Apple Inc|167=CS";
857        let raw_alloc_group =
858            "78=2|79=ACC001|661=1|80=5000|12=100|13=3|79=ACC002|661=1|80=5000|12=75|13=2";
859        let raw = format!(
860            "8=FIX.4.4|9=222|35=J|49=SELLSIDE|56=BUYSIDE|34=100|52=20251023-14:30:00|70=ALLOC001|71=0|626=1|857=0|54=1|{raw_instrument}|53=10000|6=125|75=20251023|{raw_alloc_group}|10=068|"
861        );
862        let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
863        let message = builder.build(raw.as_bytes()).into_message().unwrap();
864
865        let alloc_1 = message.get_group(fix44::NO_ALLOCS, 0).unwrap();
866        assert_eq!(alloc_1.get::<&str>(fix44::ALLOC_ACCOUNT).unwrap(), "ACC001");
867        assert_eq!(alloc_1.get::<f64>(fix44::COMMISSION).unwrap(), 100.0);
868        assert_eq!(alloc_1.get::<&str>(fix44::COMM_TYPE).unwrap(), "3");
869
870        let alloc_2 = message.get_group(fix44::NO_ALLOCS, 1).unwrap();
871        assert_eq!(alloc_2.get::<&str>(fix44::ALLOC_ACCOUNT).unwrap(), "ACC002");
872        assert_eq!(alloc_2.get::<f64>(fix44::COMMISSION).unwrap(), 75.0);
873        assert_eq!(alloc_2.get::<&str>(fix44::COMM_TYPE).unwrap(), "2");
874    }
875}