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