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
14const USER_DEFINED_TAG_MIN: u32 = 5000;
15
16const CHECKSUM_LENGTH: usize = 7;
26
27pub struct MessageBuilder {
28 dict: Dictionary,
29 header_tags: HashSet<TagU32>,
30 trailer_tags: HashSet<TagU32>,
31 message_specification: HashMap<String, MessageSpecification>,
32 config: Config,
33}
34
35impl MessageBuilder {
36 pub fn new(dict: Dictionary, config: Config) -> ParserResult<Self> {
37 let header_tags = Self::get_tags_for_component(&dict, "StandardHeader")?;
38 let trailer_tags = Self::get_tags_for_component(&dict, "StandardTrailer")?;
39 let message_definitions = build_message_specifications(&dict)?;
40
41 let parser = Self {
42 dict,
43 header_tags,
44 trailer_tags,
45 message_specification: message_definitions,
46 config,
47 };
48
49 Ok(parser)
50 }
51
52 pub fn build(&self, data: &[u8]) -> ParsedMessage {
53 let mut parser = Parser {
54 position: 0,
55 raw_data: data,
56 config: &self.config,
57 };
58 let (mut header, mut trailer) = match self.verify_integrity(&mut parser) {
59 Ok((header, trailer)) => (header, trailer),
60 Err(err) => return err.into(),
61 };
62
63 let next = match self.build_header(&mut header, &mut parser) {
64 Ok(next_field) => next_field,
65 Err(err) => {
66 return parser_error_to_parsed_message(err, header);
67 }
68 };
69
70 #[allow(clippy::expect_used)]
71 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");
72 let (body, next) = match self.build_body(msg_type, &mut parser, next) {
73 Ok((body, field)) => (body, field),
74 Err(err) => {
75 return parser_error_to_parsed_message(err, header);
76 }
77 };
78
79 self.build_trailer(&mut trailer, &mut parser, next);
80
81 let msg = Message {
82 header,
83 body,
84 trailer,
85 };
86
87 ParsedMessage::Valid(msg)
88 }
89
90 fn verify_integrity(
91 &self,
92 parser: &mut Parser,
93 ) -> Result<(Header, Trailer), MessageIntegrityError> {
94 let mut header = Header::default();
95
96 let begin_string_field = self.parse_begin_string(parser)?;
98 header.fields.insert(begin_string_field);
99
100 let body_length_field = self.parse_body_length(parser)?;
102 header.fields.insert(body_length_field);
103
104 let body_length = if let Ok(body_length) = header.get::<usize>(BODY_LENGTH) {
106 let expected_length = parser.position + body_length + CHECKSUM_LENGTH;
107 if parser.raw_data.len() != expected_length {
108 return Err(MessageIntegrityError::InvalidBodyLength);
109 }
110 body_length
111 } else {
112 return Err(MessageIntegrityError::InvalidBodyLength);
114 };
115
116 let mut trailer = Trailer::default();
118 let checksum_field = parser.parse_checksum(parser.position + body_length)?;
119 trailer.fields.insert(checksum_field);
120
121 if let Ok(checksum) = trailer.get::<CheckSum>(CHECK_SUM) {
122 let computed_checksum =
123 CheckSum::compute(&parser.raw_data[0..parser.position + body_length]);
124 if computed_checksum != checksum {
125 return Err(MessageIntegrityError::InvalidCheckSum);
126 }
127 }
128
129 let msg_type_field = self.parse_message_type(parser)?;
131 header.fields.insert(msg_type_field);
132
133 Ok((header, trailer))
134 }
135
136 fn parse_begin_string(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
137 if let Some(begin_string) = parser.next_field()
138 && begin_string.tag.get() == BEGIN_STRING.tag
139 {
140 Ok(begin_string)
141 } else {
142 Err(MessageIntegrityError::InvalidBeginString)
143 }
144 }
145
146 fn parse_body_length(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
147 if let Some(body_length) = parser.next_field()
148 && body_length.tag.get() == BODY_LENGTH.tag
149 {
150 Ok(body_length)
151 } else {
152 Err(MessageIntegrityError::InvalidBodyLength)
153 }
154 }
155
156 fn parse_message_type(&self, parser: &mut Parser) -> Result<Field, MessageIntegrityError> {
157 if let Some(msg_type) = parser.next_field()
158 && msg_type.tag.get() == MSG_TYPE.tag
159 {
160 Ok(msg_type)
161 } else {
162 Err(MessageIntegrityError::InvalidMsgType)
163 }
164 }
165
166 fn build_header(&self, header: &mut Header, parser: &mut Parser) -> ParserResult<Field> {
167 loop {
170 let field = parser.next_field().ok_or(ParserError::Malformed(
171 "message ended within header".to_string(),
172 ))?;
173
174 if self.is_header_tag(field.tag) {
175 header.fields.insert(field);
176 } else {
177 #[allow(clippy::expect_used)]
180 let msg_type = header
181 .get::<&str>(MSG_TYPE)
182 .expect("this never fails as we've verified the integrity of the header");
183 if self.dict.message_by_msgtype(msg_type).is_none() {
184 return Err(ParserError::InvalidMsgType(msg_type.to_string()));
185 }
186
187 return Ok(field);
188 }
189 }
190 }
191
192 fn build_body(
193 &self,
194 msg_type: &str,
195 parser: &mut Parser,
196 next_field: Field,
197 ) -> ParserResult<(Body, Field)> {
198 let message_def = self.get_message_def(msg_type)?;
199 let mut body = Body::default();
200 let mut field = next_field;
201
202 loop {
203 if message_def.contains_tag(field.tag) {
204 let tag = field.tag;
205 body.store_field(field);
206
207 let field_def = self.get_dict_field_by_tag(tag.get())?;
209 match message_def.get_group(tag) {
210 Some(group_def) => {
211 let (groups, next) =
212 self.parse_groups(parser, group_def, field_def.tag())?;
213 Self::check_required_fields_in_groups(&groups, group_def)?;
214 body.set_groups(groups).map_err(|err| {
215 ParserError::Malformed(format!(
216 "failed to set groups for tag {}: {err}",
217 tag.get()
218 ))
219 })?;
220 field = next;
221 }
222 None => {
223 field = parser.next_field().ok_or(ParserError::Malformed(
224 "message ended within the body".to_string(),
225 ))?;
226 }
227 }
228 } else if self.should_accept_unknown_user_defined(field.tag) {
229 body.store_field(field);
230 field = parser.next_field().ok_or(ParserError::Malformed(
231 "message ended within the body".to_string(),
232 ))?;
233 } else {
234 break;
235 }
236 }
237
238 if !self.is_trailer_tag(field.tag) {
239 return Err(ParserError::InvalidField(field.tag.get()));
240 }
241
242 Ok((body, field))
243 }
244
245 fn should_accept_unknown_user_defined(&self, tag: TagU32) -> bool {
246 !self.config.validate_user_defined_fields
247 && Self::is_user_defined(tag)
248 && self.dict.field_by_tag(tag.get()).is_none()
249 }
250
251 fn is_user_defined(tag: TagU32) -> bool {
252 tag.get() >= USER_DEFINED_TAG_MIN
253 }
254
255 fn build_trailer(&self, trailer: &mut Trailer, parser: &mut Parser, next_field: Field) {
256 let mut field = Some(next_field);
257 while let Some(f) = field {
258 if f.tag.get() == CHECK_SUM.tag {
259 break;
260 }
261 trailer.store_field(f);
262 field = parser.next_field();
263 }
264 }
265
266 fn parse_groups(
267 &self,
268 parser: &mut Parser,
269 group_def: &GroupSpecification,
270 start_tag: TagU32,
271 ) -> ParserResult<(Vec<RepeatingGroup>, Field)> {
272 let mut groups = vec![];
273 let delimiter_tag = group_def.delimiter_tag();
274
275 let mut field = parser.next_field().ok_or(ParserError::Malformed(
276 "missing delimiter field".to_string(),
277 ))?;
278
279 if field.tag != delimiter_tag {
280 return Err(ParserError::InvalidGroupFieldOrder {
281 tag: field.tag.get(),
282 group_tag: group_def.number_of_entries_tag().get(),
283 });
284 }
285
286 let mut current_group: Option<RepeatingGroup> = None;
287 let mut next_declared_idx: usize = 0;
288
289 loop {
290 let current_tag = field.tag;
291
292 if current_tag == delimiter_tag {
293 if let Some(g) = current_group.take() {
295 groups.push(g);
296 }
297 let mut new_group = RepeatingGroup::new_with_tags(start_tag, delimiter_tag);
298 new_group.store_field(field);
299 current_group = Some(new_group);
300 next_declared_idx = 1;
301 field = parser
302 .next_field()
303 .ok_or(ParserError::Malformed("incomplete group".to_string()))?;
304 } else if group_def.contains_tag(current_tag) {
305 #[allow(clippy::expect_used)]
307 let position = group_def
308 .fields()
309 .iter()
310 .position(|f| f.tag == current_tag)
311 .expect("contains_tag guarantees the tag is present in fields()");
312 if position < next_declared_idx {
313 return Err(ParserError::InvalidGroupFieldOrder {
314 tag: current_tag.get(),
315 group_tag: group_def.number_of_entries_tag().get(),
316 });
317 }
318 next_declared_idx = position + 1;
319
320 #[allow(clippy::expect_used)]
321 let group = current_group
322 .as_mut()
323 .expect("the delimiter opens a group before any declared field is seen");
324 group.store_field(field);
325
326 field = if let Some(nested_group_def) = group_def.get_nested_group(current_tag) {
327 let (nested_groups, next) =
328 self.parse_groups(parser, nested_group_def, current_tag)?;
329 #[allow(clippy::expect_used)]
330 group.set_groups(nested_groups).expect(
331 "parse_groups yields a non-empty run of entries sharing one start and delimiter tag",
332 );
333 next
334 } else {
335 parser
336 .next_field()
337 .ok_or(ParserError::Malformed("incomplete group".to_string()))?
338 };
339 } else if self.should_accept_unknown_user_defined(current_tag) {
340 #[allow(clippy::expect_used)]
341 let group = current_group
342 .as_mut()
343 .expect("the delimiter opens a group before any user-defined field is seen");
344 group.store_field(field);
345 field = parser
346 .next_field()
347 .ok_or(ParserError::Malformed("incomplete group".to_string()))?;
348 } else {
349 if let Some(g) = current_group.take() {
350 groups.push(g);
351 }
352 return Ok((groups, field));
353 }
354 }
355 }
356
357 fn check_required_fields_in_groups(
358 groups: &[RepeatingGroup],
359 group_def: &GroupSpecification,
360 ) -> ParserResult<()> {
361 let group_tag = group_def.number_of_entries_tag().get();
362 for entry in groups {
363 let field_map = entry.get_fields();
364
365 for field_def in group_def.fields() {
366 if field_def.is_required && !field_map.fields.contains_key(&field_def.tag) {
367 return Err(ParserError::RequiredFieldMissing {
368 tag: field_def.tag.get(),
369 group_tag: Some(group_tag),
370 });
371 }
372 }
373
374 for (nested_tag, nested_spec) in &group_def.nested_groups {
375 if let Some(nested_entries) = field_map.groups.get(nested_tag) {
376 Self::check_required_fields_in_groups(nested_entries, nested_spec)?;
377 }
378 }
379 }
380 Ok(())
381 }
382
383 fn get_dict_field_by_tag(&self, tag: u32) -> ParserResult<hotfix_dictionary::Field<'_>> {
384 self.dict
385 .field_by_tag(tag)
386 .ok_or(ParserError::InvalidField(tag))
387 }
388
389 fn is_header_tag(&self, tag: TagU32) -> bool {
390 self.header_tags.contains(&tag)
391 }
392
393 fn is_trailer_tag(&self, tag: TagU32) -> bool {
394 self.trailer_tags.contains(&tag)
395 }
396
397 fn get_message_def(&self, msg_type: &str) -> ParserResult<&MessageSpecification> {
398 match self.message_specification.get(msg_type) {
399 Some(message_def) => Ok(message_def),
400 None => Err(ParserError::InvalidMsgType(msg_type.to_string())),
401 }
402 }
403
404 fn get_tags_for_component(
405 dict: &Dictionary,
406 component_name: &str,
407 ) -> ParserResult<HashSet<TagU32>> {
408 let mut tags = HashSet::new();
409 let component = dict
410 .component_by_name(component_name)
411 .ok_or_else(|| ParserError::InvalidComponent(component_name.to_string()))?;
412 for item in component.items() {
413 if let LayoutItemKind::Field(field) = item.kind() {
414 tags.insert(field.tag());
415 }
416 }
417
418 Ok(tags)
419 }
420}
421
422struct Parser<'a> {
423 position: usize,
424 raw_data: &'a [u8],
425 config: &'a Config,
426}
427
428impl<'a> Parser<'a> {
429 fn next_field(&mut self) -> Option<Field> {
430 let (field, end_position) = self.parse_field_at(self.position)?;
431 self.position = end_position + 1;
432
433 Some(field)
434 }
435
436 fn parse_field_at(&self, position: usize) -> Option<(Field, usize)> {
437 let mut iter = self.raw_data[position..].iter();
438 let equal_sign_position = position + iter.position(|c| *c == b'=')?;
439 let bytes_until_separator = iter.position(|c| *c == self.config.separator)?;
440 let separator_position = equal_sign_position + bytes_until_separator + 1;
441
442 let tag = tag_from_bytes(&self.raw_data[position..equal_sign_position])?;
443 let data = self.raw_data[equal_sign_position + 1..separator_position].to_vec();
444 let field = Field::new(tag, data);
445
446 Some((field, separator_position))
447 }
448
449 fn parse_checksum(&self, checksum_start: usize) -> Result<Field, MessageIntegrityError> {
450 if let Some((checksum, _)) = self.parse_field_at(checksum_start)
451 && checksum.tag.get() == CHECK_SUM.tag
452 {
453 Ok(checksum)
454 } else {
455 Err(MessageIntegrityError::InvalidCheckSum)
456 }
457 }
458}
459
460fn tag_from_bytes(bytes: &[u8]) -> Option<TagU32> {
461 let mut tag = 0u32;
462 for byte in bytes.iter().copied() {
463 tag = tag * 10 + (byte as u32 - b'0' as u32);
464 }
465
466 TagU32::new(tag)
467}
468
469fn parser_error_to_parsed_message(err: ParserError, header: Header) -> ParsedMessage {
470 match err {
471 ParserError::IOError(_) => ParsedMessage::Garbled(GarbledReason::Malformed),
472 ParserError::InvalidField(tag) => ParsedMessage::Invalid {
473 reason: InvalidReason::InvalidField(tag),
474 message: Message::with_header(header),
475 },
476 ParserError::InvalidGroup(tag) => ParsedMessage::Invalid {
477 reason: InvalidReason::InvalidGroup(tag),
478 message: Message::with_header(header),
479 },
480 ParserError::InvalidGroupFieldOrder { tag, group_tag } => ParsedMessage::Invalid {
481 reason: InvalidReason::InvalidOrderInGroup { tag, group_tag },
482 message: Message::with_header(header),
483 },
484 ParserError::InvalidComponent(tag) => ParsedMessage::Invalid {
485 reason: InvalidReason::InvalidComponent(tag),
486 message: Message::with_header(header),
487 },
488 ParserError::InvalidMsgType(msg_type) => ParsedMessage::Invalid {
489 reason: InvalidReason::InvalidMsgType(msg_type),
490 message: Message::with_header(header),
491 },
492 ParserError::RequiredFieldMissing { tag, group_tag } => ParsedMessage::Invalid {
493 reason: InvalidReason::RequiredFieldMissing { tag, group_tag },
494 message: Message::with_header(header),
495 },
496 ParserError::Malformed(_) => ParsedMessage::Garbled(GarbledReason::Malformed),
497 }
498}
499
500struct FieldSpecification {
501 pub(crate) tag: TagU32,
502 pub(crate) is_required: bool,
503}
504
505struct GroupSpecification {
506 number_of_entries_tag: TagU32,
507 fields: Vec<FieldSpecification>,
508 nested_groups: HashMap<TagU32, GroupSpecification>,
509}
510
511impl GroupSpecification {
512 pub fn fields(&self) -> &[FieldSpecification] {
513 self.fields.as_slice()
514 }
515 pub fn number_of_entries_tag(&self) -> TagU32 {
516 self.number_of_entries_tag
517 }
518
519 pub fn delimiter_tag(&self) -> TagU32 {
520 #[allow(clippy::expect_used)]
521 self.fields
522 .first()
523 .expect("groups always have at least one field")
524 .tag
525 }
526
527 pub fn contains_tag(&self, tag: TagU32) -> bool {
528 self.fields.iter().any(|f| f.tag == tag)
529 }
530
531 pub fn get_nested_group(&self, tag: TagU32) -> Option<&GroupSpecification> {
532 self.nested_groups.get(&tag)
533 }
534}
535
536struct MessageSpecification {
537 fields: Vec<FieldSpecification>,
538 groups: HashMap<TagU32, GroupSpecification>,
539}
540
541impl MessageSpecification {
542 pub fn contains_tag(&self, tag: TagU32) -> bool {
543 self.fields.iter().any(|f| f.tag == tag)
544 }
545
546 pub fn get_group(&self, tag: TagU32) -> Option<&GroupSpecification> {
547 self.groups.get(&tag)
548 }
549}
550
551fn build_message_specifications(
552 dict: &Dictionary,
553) -> ParserResult<HashMap<String, MessageSpecification>> {
554 let mut definitions = HashMap::new();
555
556 for message in dict.messages() {
557 let fields = message
558 .layout()
559 .flat_map(|item| extract_fields(dict, item))
560 .flatten()
561 .collect();
562
563 let mut groups = HashMap::new();
564 for item in message.layout() {
565 groups.extend(extract_groups(dict, item)?);
566 }
567
568 let message_def = MessageSpecification { fields, groups };
569 definitions.insert(message.msg_type().to_string(), message_def);
570 }
571
572 Ok(definitions)
573}
574
575fn extract_fields(dict: &Dictionary, item: LayoutItem) -> ParserResult<Vec<FieldSpecification>> {
576 let is_required = item.required();
577 let fields = match item.kind() {
578 LayoutItemKind::Component(c) => {
579 let component = dict
580 .component_by_name(c.name())
581 .ok_or_else(|| ParserError::InvalidComponent(c.name().to_string()))?;
582 component
583 .items()
584 .flat_map(|i| extract_fields(dict, i))
585 .flatten()
586 .collect()
587 }
588 LayoutItemKind::Field(field) => vec![FieldSpecification {
589 tag: field.tag(),
590 is_required,
591 }],
592 LayoutItemKind::Group(field, _) => vec![FieldSpecification {
593 tag: field.tag(),
594 is_required,
595 }],
596 };
597
598 Ok(fields)
599}
600
601fn extract_groups(
602 dict: &Dictionary,
603 item: LayoutItem,
604) -> ParserResult<HashMap<TagU32, GroupSpecification>> {
605 let mut groups = HashMap::new();
606 match item.kind() {
607 LayoutItemKind::Component(c) => {
608 let component = dict
609 .component_by_name(c.name())
610 .ok_or_else(|| ParserError::InvalidComponent(c.name().to_string()))?;
611 for i in component.items() {
612 groups.extend(extract_groups(dict, i)?);
613 }
614 }
615 LayoutItemKind::Group(field, items) => {
616 let mut nested_groups = HashMap::new();
617 for i in items.iter() {
618 nested_groups.extend(extract_groups(dict, i.clone())?);
619 }
620 groups.insert(
621 field.tag(),
622 GroupSpecification {
623 number_of_entries_tag: field.tag(),
624 fields: items
625 .iter()
626 .flat_map(|i| extract_fields(dict, i.clone()))
627 .flatten()
628 .collect(),
629 nested_groups,
630 },
631 );
632 }
633 _ => {}
634 };
635
636 Ok(groups)
637}
638
639#[cfg(test)]
640mod tests {
641 use crate::builder::MessageBuilder;
642 use crate::field_types::Currency;
643 use crate::message::Config;
644 use crate::parsed_message::{GarbledReason, InvalidReason, ParsedMessage};
645 use crate::{Part, fix44};
646 use hotfix_dictionary::{Dictionary, IsFieldDefinition, TagU32};
647
648 const CONFIG: Config = Config::with_separator(b'|');
649
650 #[test]
651 fn test_specification_top_level_fields() {
652 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
653 let message_def = builder.get_message_def("J").unwrap();
654
655 assert!(message_def.contains_tag(fix44::SYMBOL.tag()));
657
658 assert!(message_def.contains_tag(fix44::NO_ORDERS.tag()));
660
661 assert!(!message_def.contains_tag(fix44::ORDER_QTY.tag()));
663 }
664
665 #[test]
666 fn test_specification_top_level_groups() {
667 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
668 let message_def = builder.get_message_def("J").unwrap();
669
670 let expected_group_fields = vec![
673 fix44::NO_ORDERS,
674 fix44::NO_ALLOCS,
675 fix44::NO_EXECS,
676 fix44::NO_STIPULATIONS,
677 fix44::NO_PARTY_I_DS,
678 fix44::NO_SECURITY_ALT_ID,
679 fix44::NO_LEGS,
680 fix44::NO_UNDERLYINGS,
681 fix44::NO_EVENTS,
682 fix44::NO_INSTR_ATTRIB,
683 ];
684 assert_eq!(message_def.groups.len(), expected_group_fields.len());
685 for field in expected_group_fields {
686 assert!(
687 message_def
688 .get_group(TagU32::new(field.tag).unwrap())
689 .is_some()
690 );
691 }
692
693 assert!(
695 message_def
696 .get_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
697 .is_none()
698 );
699 }
700
701 #[test]
702 fn test_specification_nested_groups() {
703 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
704 let message_def = builder.get_message_def("J").unwrap();
705
706 let order_alloc_group = message_def.get_group(fix44::NO_ORDERS.tag()).unwrap();
708 assert_eq!(order_alloc_group.nested_groups.len(), 1);
709 let nested_parties_2_group = order_alloc_group
710 .get_nested_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
711 .expect("nested parties group to exist");
712
713 assert_eq!(nested_parties_2_group.nested_groups.len(), 1);
715 let subgroup = nested_parties_2_group
716 .get_nested_group(fix44::NO_NESTED2_PARTY_SUB_I_DS.tag())
717 .expect("parties subgroup to exist");
718 assert!(subgroup.nested_groups.is_empty());
719 }
720
721 #[test]
722 fn test_specification_field_order_in_nested_group() {
723 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
724 let message_def = builder.get_message_def("J").unwrap();
725
726 let order_alloc_group = message_def.get_group(fix44::NO_ORDERS.tag()).unwrap();
728 assert_eq!(order_alloc_group.nested_groups.len(), 1);
729 let nested_parties_2_group = order_alloc_group
730 .get_nested_group(fix44::NO_NESTED2_PARTY_I_DS.tag())
731 .expect("nested parties group to exist");
732
733 let mut fields = nested_parties_2_group.fields.iter();
734 let expected_fields = vec![
735 (fix44::NESTED2_PARTY_ID, false),
736 (fix44::NESTED2_PARTY_ID_SOURCE, false),
737 (fix44::NESTED2_PARTY_ROLE, false),
738 (fix44::NO_NESTED2_PARTY_SUB_I_DS, false),
739 ];
740
741 for (field_definition, is_required) in expected_fields {
742 let next = fields.next().unwrap();
743 assert_eq!(next.tag.get(), field_definition.tag);
744 assert_eq!(next.is_required, is_required);
745 }
746 }
747
748 #[test]
749 fn parse_simple_message() {
750 let raw = b"8=FIX.4.4|9=40|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=093|";
751 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
752
753 let message = builder.build(raw).into_message().unwrap();
754
755 let begin: &str = message.header().get(fix44::BEGIN_STRING).unwrap();
756 assert_eq!(begin, "FIX.4.4");
757
758 let body_length: u32 = message.header().get(fix44::BODY_LENGTH).unwrap();
759 assert_eq!(body_length, 40);
760
761 let message_type: &str = message.header().get(fix44::MSG_TYPE).unwrap();
762 assert_eq!(message_type, "D");
763
764 let currency: &Currency = message.get(fix44::CURRENCY).unwrap();
765 assert_eq!(currency, b"USD");
766
767 let time_in_force: &str = message.get(fix44::TIME_IN_FORCE).unwrap();
768 assert_eq!(time_in_force, "0");
769
770 let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
771 assert_eq!(checksum, "093");
772 }
773
774 #[test]
775 fn repeating_group_entries() {
776 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|";
777 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
778
779 let message = builder.build(raw).into_message().unwrap();
780
781 let begin: &str = message.header().get(fix44::BEGIN_STRING).unwrap();
782 assert_eq!(begin, "FIX.4.4");
783
784 let fee1 = message.get_group(fix44::NO_MISC_FEES, 0).unwrap();
785 let amt: f64 = fee1.get(fix44::MISC_FEE_AMT).unwrap();
786 assert_eq!(amt, 100.0);
787
788 let fee2 = message.get_group(fix44::NO_MISC_FEES, 1).unwrap();
789 let fee_type: &str = fee2.get(fix44::MISC_FEE_TYPE).unwrap();
790 assert_eq!(fee_type, "7");
791
792 let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
793 assert_eq!(checksum, "140");
794 }
795
796 #[test]
797 fn nested_repeating_group_entries() {
798 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|";
799 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
800 let message = builder.build(raw).into_message().unwrap();
801
802 let party_a = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
803 let party_a_0 = party_a
804 .get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0)
805 .unwrap();
806 let sub_id_0: &str = party_a_0.get(fix44::PARTY_SUB_ID).unwrap();
807 assert_eq!(sub_id_0, "SUBPARTYA1");
808
809 let party_b = message.get_group(fix44::NO_PARTY_I_DS, 1).unwrap();
810 let party_b_id: &str = party_b.get(fix44::PARTY_ID).unwrap();
811 assert_eq!(party_b_id, "PARTYB");
812
813 let party_b_role: u32 = party_b.get(fix44::PARTY_ROLE).unwrap();
814 assert_eq!(party_b_role, 2);
815
816 let checksum: &str = message.trailer().get(fix44::CHECK_SUM).unwrap();
817 assert_eq!(checksum, "129");
818 }
819
820 #[test]
821 fn test_begin_string_not_the_first_tag() {
822 let raw = b"9=40|8=FIX.4.4|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=093|";
823 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
824 let parsed_message = builder.build(raw);
825
826 assert!(matches!(
827 parsed_message,
828 ParsedMessage::Garbled(GarbledReason::InvalidBeginString)
829 ));
830 }
831
832 #[test]
833 fn test_body_length_not_the_second_tag() {
834 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|";
835 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
836 let parsed_message = builder.build(raw);
837
838 assert!(matches!(
839 parsed_message,
840 ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
841 ));
842 }
843
844 #[test]
845 fn test_body_length_is_wrong() {
846 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|";
847 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
848 let parsed_message = builder.build(raw);
849
850 assert!(matches!(
851 parsed_message,
852 ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
853 ));
854 }
855
856 #[test]
857 fn test_body_length_exceeds_message_length() {
858 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|";
859 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
860 let parsed_message = builder.build(raw);
861
862 assert!(matches!(
863 parsed_message,
864 ParsedMessage::Garbled(GarbledReason::InvalidBodyLength)
865 ));
866 }
867
868 #[test]
869 fn test_msg_type_is_not_the_third_tag() {
870 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|";
871 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
872 let parsed_message = builder.build(raw);
873
874 assert!(matches!(
875 parsed_message,
876 ParsedMessage::Garbled(GarbledReason::InvalidMsgType)
877 ));
878 }
879
880 #[test]
881 fn test_checksum_is_not_the_last_tag() {
882 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|";
883 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
884 let parsed_message = builder.build(raw);
885
886 assert!(matches!(
887 parsed_message,
888 ParsedMessage::Garbled(GarbledReason::InvalidChecksum)
889 ));
890 }
891
892 #[test]
893 fn test_invalid_checksum() {
894 let raw = b"8=FIX.4.4|9=40|35=D|49=AFUNDMGR|56=ABROKER|15=USD|59=0|10=000|";
895 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
896 let parsed_message = builder.build(raw);
897
898 assert!(matches!(
899 parsed_message,
900 ParsedMessage::Garbled(GarbledReason::InvalidChecksum)
901 ));
902 }
903
904 #[test]
905 fn test_invalid_field_in_body() {
906 let raw = b"8=FIX.4.4|9=53|35=D|49=AFUNDMGR|9999=invalid|56=ABROKER|15=USD|59=0|10=229|";
907 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
908 let parsed_message = builder.build(raw);
909
910 assert!(matches!(
911 parsed_message,
912 ParsedMessage::Invalid {
913 reason: InvalidReason::InvalidField(_),
914 ..
915 }
916 ));
917 }
918
919 #[test]
920 fn test_invalid_group_in_body() {
921 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|";
924 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
925 let parsed_message = builder.build(raw);
926
927 assert!(matches!(
928 parsed_message,
929 ParsedMessage::Invalid {
930 reason: InvalidReason::InvalidOrderInGroup {
931 tag: 385,
932 group_tag: 384
933 },
934 ..
935 }
936 ));
937 }
938
939 #[test]
940 fn test_parsing_nested_component_inside_group() {
941 let raw_instrument = "55=AAPL|107=Apple Inc|167=CS";
943 let raw_alloc_group =
944 "78=2|79=ACC001|661=1|80=5000|12=100|13=3|79=ACC002|661=1|80=5000|12=75|13=2";
945 let raw = format!(
946 "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|"
947 );
948 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
949 let message = builder.build(raw.as_bytes()).into_message().unwrap();
950
951 let alloc_1 = message.get_group(fix44::NO_ALLOCS, 0).unwrap();
952 assert_eq!(alloc_1.get::<&str>(fix44::ALLOC_ACCOUNT).unwrap(), "ACC001");
953 assert_eq!(alloc_1.get::<f64>(fix44::COMMISSION).unwrap(), 100.0);
954 assert_eq!(alloc_1.get::<&str>(fix44::COMM_TYPE).unwrap(), "3");
955
956 let alloc_2 = message.get_group(fix44::NO_ALLOCS, 1).unwrap();
957 assert_eq!(alloc_2.get::<&str>(fix44::ALLOC_ACCOUNT).unwrap(), "ACC002");
958 assert_eq!(alloc_2.get::<f64>(fix44::COMMISSION).unwrap(), 75.0);
959 assert_eq!(alloc_2.get::<&str>(fix44::COMM_TYPE).unwrap(), "2");
960 }
961
962 const CONFIG_NO_USER_VALIDATION: Config =
963 Config::with_separator(b'|').validate_user_defined_fields(false);
964
965 fn build_pipe_separated_message(content: &str) -> Vec<u8> {
966 let body_length = content.len();
967 let prefix = format!("8=FIX.4.4|9={body_length}|{content}");
968 let checksum: u8 = prefix.bytes().fold(0u8, |acc, x| acc.wrapping_add(x));
969 format!("{prefix}10={checksum:03}|").into_bytes()
970 }
971
972 #[test]
973 fn test_unknown_user_defined_tag_at_body_rejected_by_default() {
974 let raw =
975 build_pipe_separated_message("35=D|49=SENDER|56=TARGET|55=AAPL|59=0|20000=custom|");
976 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
977 let parsed = builder.build(&raw);
978
979 assert!(matches!(
980 parsed,
981 ParsedMessage::Invalid {
982 reason: InvalidReason::InvalidField(20000),
983 ..
984 }
985 ));
986 }
987
988 #[test]
989 fn test_unknown_user_defined_tag_at_body_accepted_when_validation_disabled() {
990 let raw =
991 build_pipe_separated_message("35=D|49=SENDER|56=TARGET|55=AAPL|59=0|20000=custom|");
992 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
993 let message = builder.build(&raw).into_message().unwrap();
994
995 let custom_tag = TagU32::new(20000).unwrap();
996 let custom_value = message.get_field_map().get_raw(custom_tag).unwrap();
997 assert_eq!(custom_value, b"custom");
998
999 assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1001 }
1002
1003 #[test]
1004 fn test_unknown_user_defined_tag_inside_group_stays_in_group_when_validation_disabled() {
1005 let raw = build_pipe_separated_message(
1006 "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|20000=custom|55=AAPL|",
1007 );
1008 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1009 let message = builder.build(&raw).into_message().unwrap();
1010
1011 let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1015 assert_eq!(party.get::<&str>(fix44::PARTY_ID).unwrap(), "PARTYA");
1016 assert!(message.get_group(fix44::NO_PARTY_I_DS, 1).is_none());
1017
1018 let custom_tag = TagU32::new(20000).unwrap();
1019 let in_group = party.get_field_map().get_raw(custom_tag).unwrap();
1020 assert_eq!(in_group, b"custom");
1021
1022 assert!(message.get_field_map().get_raw(custom_tag).is_none());
1024
1025 assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1027 }
1028
1029 #[test]
1030 fn test_unknown_user_defined_tag_inside_group_rejected_when_validation_enabled() {
1031 let raw = build_pipe_separated_message(
1032 "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|20000=custom|55=AAPL|",
1033 );
1034 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1035 let parsed = builder.build(&raw);
1036
1037 assert!(matches!(
1038 parsed,
1039 ParsedMessage::Invalid {
1040 reason: InvalidReason::InvalidField(20000),
1041 ..
1042 }
1043 ));
1044 }
1045
1046 #[test]
1047 fn test_unknown_user_defined_tag_inside_nested_group_stays_in_nested_group() {
1048 let raw = build_pipe_separated_message(
1053 "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|802=1|523=SUB1|803=1|20000=custom|",
1054 );
1055 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1056 let message = builder.build(&raw).into_message().unwrap();
1057
1058 let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1059 let sub = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0).unwrap();
1060 assert_eq!(sub.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB1");
1061
1062 let custom_tag = TagU32::new(20000).unwrap();
1063 let in_sub = sub.get_field_map().get_raw(custom_tag).unwrap();
1065 assert_eq!(in_sub, b"custom");
1066 assert!(party.get_field_map().get_raw(custom_tag).is_none());
1068 assert!(message.get_field_map().get_raw(custom_tag).is_none());
1070 }
1071
1072 #[test]
1073 fn test_group_rejects_when_first_tag_is_not_delimiter() {
1074 let raw = build_pipe_separated_message(
1078 "35=D|49=SENDER|56=TARGET|453=1|20000=x|448=PARTYA|447=D|452=1|",
1079 );
1080 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1081 let parsed = builder.build(&raw);
1082
1083 assert!(matches!(
1084 parsed,
1085 ParsedMessage::Invalid {
1086 reason: InvalidReason::InvalidOrderInGroup {
1087 tag: 20000,
1088 group_tag: 453,
1089 },
1090 ..
1091 }
1092 ));
1093 }
1094
1095 #[test]
1096 fn test_known_outer_field_after_group_fields_ends_group() {
1097 let raw = build_pipe_separated_message(
1098 "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|55=AAPL|59=0|",
1099 );
1100 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1101 let message = builder.build(&raw).into_message().unwrap();
1102
1103 let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1104 assert_eq!(party.get::<&str>(fix44::PARTY_ID).unwrap(), "PARTYA");
1105 assert!(message.get_group(fix44::NO_PARTY_I_DS, 1).is_none());
1106
1107 assert!(party.get_raw(fix44::SYMBOL).is_none());
1109 assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1110 assert_eq!(message.get::<&str>(fix44::TIME_IN_FORCE).unwrap(), "0");
1111 }
1112
1113 #[test]
1114 fn test_nested_and_outer_group_end_on_body_field() {
1115 let raw = build_pipe_separated_message(
1116 "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|802=1|523=SUB1|803=1|55=AAPL|59=0|",
1117 );
1118 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1119 let message = builder.build(&raw).into_message().unwrap();
1120
1121 let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1122 let sub = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0).unwrap();
1123 assert_eq!(sub.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB1");
1124
1125 assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1126 assert_eq!(message.get::<&str>(fix44::TIME_IN_FORCE).unwrap(), "0");
1127 }
1128
1129 #[test]
1130 fn test_declared_group_field_out_of_order_is_rejected() {
1131 let raw =
1132 build_pipe_separated_message("35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|452=1|447=D|");
1133 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1134 let parsed = builder.build(&raw);
1135
1136 assert!(matches!(
1137 parsed,
1138 ParsedMessage::Invalid {
1139 reason: InvalidReason::InvalidOrderInGroup {
1140 tag: 447,
1141 group_tag: 453,
1142 },
1143 ..
1144 }
1145 ));
1146 }
1147
1148 #[test]
1149 fn test_multiple_nested_group_instances_are_parsed() {
1150 let raw = build_pipe_separated_message(
1151 "35=D|49=SENDER|56=TARGET|453=1|448=PARTYA|447=D|452=1|802=2|523=SUB1|803=1|523=SUB2|803=2|55=AAPL|",
1152 );
1153 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG).unwrap();
1154 let message = builder.build(&raw).into_message().unwrap();
1155
1156 let party = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1157 let sub0 = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0).unwrap();
1158 let sub1 = party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 1).unwrap();
1159 assert_eq!(sub0.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB1");
1160 assert_eq!(sub1.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUB2");
1161 assert!(party.get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 2).is_none());
1162
1163 assert_eq!(message.get::<&str>(fix44::SYMBOL).unwrap(), "AAPL");
1164 }
1165
1166 const NESTED_REQUIRED_SPEC: &str = r#"<fix type='FIX' major='4' minor='4' servicepack='0'>
1167 <header>
1168 <field name='BeginString' required='Y'/>
1169 <field name='BodyLength' required='Y'/>
1170 <field name='MsgType' required='Y'/>
1171 <field name='SenderCompID' required='Y'/>
1172 <field name='TargetCompID' required='Y'/>
1173 <field name='MsgSeqNum' required='Y'/>
1174 <field name='SendingTime' required='Y'/>
1175 </header>
1176 <trailer>
1177 <field name='CheckSum' required='Y'/>
1178 </trailer>
1179 <messages>
1180 <message name='NestedReqMsg' msgtype='U1' msgcat='app'>
1181 <group name='NoOuter' required='N'>
1182 <field name='OuterDelim' required='N'/>
1183 <group name='NoInner' required='N'>
1184 <field name='InnerDelim' required='N'/>
1185 <field name='InnerRequired' required='Y'/>
1186 </group>
1187 </group>
1188 <field name='CustomBody' required='N'/>
1189 </message>
1190 </messages>
1191 <components>
1192 </components>
1193 <fields>
1194 <field number='8' name='BeginString' type='STRING'/>
1195 <field number='9' name='BodyLength' type='LENGTH'/>
1196 <field number='35' name='MsgType' type='STRING'/>
1197 <field number='49' name='SenderCompID' type='STRING'/>
1198 <field number='56' name='TargetCompID' type='STRING'/>
1199 <field number='34' name='MsgSeqNum' type='SEQNUM'/>
1200 <field number='52' name='SendingTime' type='UTCTIMESTAMP'/>
1201 <field number='10' name='CheckSum' type='STRING'/>
1202 <field number='6001' name='NoOuter' type='NUMINGROUP'/>
1203 <field number='6002' name='OuterDelim' type='STRING'/>
1204 <field number='6003' name='NoInner' type='NUMINGROUP'/>
1205 <field number='6004' name='InnerDelim' type='STRING'/>
1206 <field number='6005' name='InnerRequired' type='STRING'/>
1207 <field number='5100' name='CustomBody' type='STRING'/>
1208 </fields>
1209</fix>"#;
1210
1211 #[test]
1212 fn test_declared_user_defined_body_field_ends_group_when_validation_disabled() {
1213 let dict = Dictionary::from_quickfix_spec(NESTED_REQUIRED_SPEC).unwrap();
1214 let raw = build_pipe_separated_message(
1215 "35=U1|49=S|56=T|34=1|52=20231103-12:00:00|6001=1|6002=od|6003=1|6004=id|6005=req|5100=v|",
1216 );
1217 let builder = MessageBuilder::new(dict, CONFIG_NO_USER_VALIDATION).unwrap();
1218 let message = builder.build(&raw).into_message().unwrap();
1219
1220 let custom_tag = TagU32::new(5100).unwrap();
1221 let outer_tag = TagU32::new(6001).unwrap();
1222 let inner_tag = TagU32::new(6003).unwrap();
1223
1224 let outer = message.get_field_map().get_group(outer_tag, 0).unwrap();
1225 let inner = outer.get_group(inner_tag, 0).unwrap();
1226 assert!(inner.get_field_map().get_raw(custom_tag).is_none());
1227 assert!(outer.get_field_map().get_raw(custom_tag).is_none());
1228 assert_eq!(message.get_field_map().get_raw(custom_tag).unwrap(), b"v");
1229 }
1230
1231 #[test]
1232 fn test_required_field_missing_in_nested_group() {
1233 let dict = Dictionary::from_quickfix_spec(NESTED_REQUIRED_SPEC).unwrap();
1234 let raw = build_pipe_separated_message(
1235 "35=U1|49=S|56=T|34=1|52=20231103-12:00:00|6001=1|6002=od|6003=1|6004=id|",
1236 );
1237 let builder = MessageBuilder::new(dict, CONFIG).unwrap();
1238 let parsed = builder.build(&raw);
1239
1240 assert!(matches!(
1241 parsed,
1242 ParsedMessage::Invalid {
1243 reason: InvalidReason::RequiredFieldMissing {
1244 tag: 6005,
1245 group_tag: Some(6003),
1246 },
1247 ..
1248 }
1249 ));
1250 }
1251
1252 #[test]
1253 fn test_fields_declared_in_group_still_parsed_inside_when_validation_disabled() {
1254 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|";
1259 let builder = MessageBuilder::new(Dictionary::fix44(), CONFIG_NO_USER_VALIDATION).unwrap();
1260 let message = builder.build(raw).into_message().unwrap();
1261
1262 let party_a = message.get_group(fix44::NO_PARTY_I_DS, 0).unwrap();
1263 let sub = party_a
1264 .get_group(fix44::NO_PARTY_SUB_I_DS.tag(), 0)
1265 .unwrap();
1266 assert_eq!(sub.get::<&str>(fix44::PARTY_SUB_ID).unwrap(), "SUBPARTYA1");
1267
1268 let party_b = message.get_group(fix44::NO_PARTY_I_DS, 1).unwrap();
1269 assert_eq!(party_b.get::<&str>(fix44::PARTY_ID).unwrap(), "PARTYB");
1270 }
1271
1272 #[test]
1273 fn test_group_entry_missing_required_field_is_rejected() {
1274 let dict = Dictionary::fix44();
1275 let builder = MessageBuilder::new(dict, CONFIG).unwrap();
1276
1277 let candidate = builder
1281 .message_specification
1282 .iter()
1283 .find_map(|(msg_type, message_def)| {
1284 message_def
1285 .groups
1286 .iter()
1287 .find_map(|(group_tag, group_spec)| {
1288 let required_non_delim = group_spec
1289 .fields()
1290 .iter()
1291 .find(|f| f.is_required && f.tag != group_spec.delimiter_tag())?;
1292 Some((
1293 msg_type.clone(),
1294 group_tag.get(),
1295 group_spec.delimiter_tag().get(),
1296 required_non_delim.tag.get(),
1297 ))
1298 })
1299 });
1300
1301 let Some((msg_type, group_tag, delimiter_tag, missing_tag)) = candidate else {
1302 return;
1304 };
1305
1306 let body = format!(
1307 "35={msg_type}|49=S|56=T|34=1|52=20231103-12:00:00|{group_tag}=1|{delimiter_tag}=X|"
1308 );
1309 let raw = build_pipe_separated_message(&body);
1310 let parsed = builder.build(&raw);
1311
1312 match parsed {
1313 ParsedMessage::Invalid {
1314 reason:
1315 InvalidReason::RequiredFieldMissing {
1316 tag,
1317 group_tag: Some(g),
1318 },
1319 ..
1320 } => {
1321 assert_eq!(tag, missing_tag);
1322 assert_eq!(g, group_tag);
1323 }
1324 other => panic!(
1325 "expected RequiredFieldMissing(tag={missing_tag}, group_tag={group_tag}); \
1326 msg_type={msg_type}, delimiter={delimiter_tag}, got: {}",
1327 match &other {
1328 ParsedMessage::Valid(_) => "Valid".to_string(),
1329 ParsedMessage::Invalid { reason, .. } => match reason {
1330 InvalidReason::InvalidField(t) => format!("InvalidField({t})"),
1331 InvalidReason::InvalidGroup(t) => format!("InvalidGroup({t})"),
1332 InvalidReason::InvalidOrderInGroup { tag, group_tag } => {
1333 format!("InvalidOrderInGroup(tag={tag}, group_tag={group_tag})")
1334 }
1335 InvalidReason::InvalidComponent(s) => format!("InvalidComponent({s})"),
1336 InvalidReason::InvalidMsgType(s) => format!("InvalidMsgType({s})"),
1337 InvalidReason::RequiredFieldMissing { tag, group_tag } => {
1338 format!("RequiredFieldMissing(tag={tag}, group_tag={group_tag:?})")
1339 }
1340 },
1341 ParsedMessage::Garbled(_) => "Garbled".to_string(),
1342 ParsedMessage::UnexpectedError(_) => "UnexpectedError".to_string(),
1343 }
1344 ),
1345 }
1346 }
1347}