@@ -214,6 +214,7 @@ impl MessageBuilder {
214214 field_def. tag ( ) ,
215215 ParentScope :: Message ( message_def) ,
216216 ) ?;
217+ Self :: check_required_fields_in_groups ( & groups, group_def) ?;
217218 body. set_groups ( groups) . map_err ( |err| {
218219 ParserError :: Malformed ( format ! (
219220 "failed to set groups for tag {}: {err}" ,
@@ -386,6 +387,32 @@ impl MessageBuilder {
386387 }
387388 }
388389
390+ fn check_required_fields_in_groups (
391+ groups : & [ RepeatingGroup ] ,
392+ group_def : & GroupSpecification ,
393+ ) -> ParserResult < ( ) > {
394+ let group_tag = group_def. number_of_entries_tag ( ) . get ( ) ;
395+ for entry in groups {
396+ let field_map = entry. get_fields ( ) ;
397+
398+ for field_def in group_def. fields ( ) {
399+ if field_def. is_required && !field_map. fields . contains_key ( & field_def. tag ) {
400+ return Err ( ParserError :: RequiredFieldMissing {
401+ tag : field_def. tag . get ( ) ,
402+ group_tag : Some ( group_tag) ,
403+ } ) ;
404+ }
405+ }
406+
407+ for ( nested_tag, nested_spec) in & group_def. nested_groups {
408+ if let Some ( nested_entries) = field_map. groups . get ( nested_tag) {
409+ Self :: check_required_fields_in_groups ( nested_entries, nested_spec) ?;
410+ }
411+ }
412+ }
413+ Ok ( ( ) )
414+ }
415+
389416 fn get_dict_field_by_tag ( & self , tag : u32 ) -> ParserResult < hotfix_dictionary:: Field < ' _ > > {
390417 self . dict
391418 . field_by_tag ( tag)
@@ -495,22 +522,19 @@ fn parser_error_to_parsed_message(err: ParserError, header: Header) -> ParsedMes
495522 reason : InvalidReason :: InvalidMsgType ( msg_type) ,
496523 message : Message :: with_header ( header) ,
497524 } ,
525+ ParserError :: RequiredFieldMissing { tag, group_tag } => ParsedMessage :: Invalid {
526+ reason : InvalidReason :: RequiredFieldMissing { tag, group_tag } ,
527+ message : Message :: with_header ( header) ,
528+ } ,
498529 ParserError :: Malformed ( _) => ParsedMessage :: Garbled ( GarbledReason :: Malformed ) ,
499530 }
500531}
501532
502533struct FieldSpecification {
503534 pub ( crate ) tag : TagU32 ,
504- #[ allow( dead_code) ]
505535 pub ( crate ) is_required : bool ,
506536}
507537
508- /// The enclosing scope of a repeating-group parse.
509- ///
510- /// When a tag is encountered that does not belong to the group currently being
511- /// parsed, the parser uses the parent scope to decide whether the tag closes
512- /// the group (because it is recognised by the enclosing scope) or whether it
513- /// should be treated as part of the current group / rejected.
514538#[ derive( Clone , Copy ) ]
515539enum ParentScope < ' a > {
516540 Message ( & ' a MessageSpecification ) ,
@@ -1144,4 +1168,80 @@ mod tests {
11441168 let party_b = message. get_group ( fix44:: NO_PARTY_I_DS , 1 ) . unwrap ( ) ;
11451169 assert_eq ! ( party_b. get:: <& str >( fix44:: PARTY_ID ) . unwrap( ) , "PARTYB" ) ;
11461170 }
1171+
1172+ #[ test]
1173+ fn test_group_entry_missing_required_field_is_rejected ( ) {
1174+ let dict = Dictionary :: fix44 ( ) ;
1175+ let builder = MessageBuilder :: new ( dict, CONFIG ) . unwrap ( ) ;
1176+
1177+ // Find any (msg_type, group, required-non-delim-field) combination so
1178+ // we can construct a minimal message that's structurally well-formed
1179+ // except for the missing group field.
1180+ let candidate = builder
1181+ . message_specification
1182+ . iter ( )
1183+ . find_map ( |( msg_type, message_def) | {
1184+ message_def
1185+ . groups
1186+ . iter ( )
1187+ . find_map ( |( group_tag, group_spec) | {
1188+ let required_non_delim = group_spec
1189+ . fields ( )
1190+ . iter ( )
1191+ . find ( |f| f. is_required && f. tag != group_spec. delimiter_tag ( ) ) ?;
1192+ Some ( (
1193+ msg_type. clone ( ) ,
1194+ group_tag. get ( ) ,
1195+ group_spec. delimiter_tag ( ) . get ( ) ,
1196+ required_non_delim. tag . get ( ) ,
1197+ ) )
1198+ } )
1199+ } ) ;
1200+
1201+ let Some ( ( msg_type, group_tag, delimiter_tag, missing_tag) ) = candidate else {
1202+ // The dictionary has no group with a required non-delimiter field.
1203+ return ;
1204+ } ;
1205+
1206+ let body = format ! (
1207+ "35={msg_type}|49=S|56=T|34=1|52=20231103-12:00:00|{group_tag}=1|{delimiter_tag}=X|"
1208+ ) ;
1209+ let raw = build_pipe_separated_message ( & body) ;
1210+ let parsed = builder. build ( & raw ) ;
1211+
1212+ match parsed {
1213+ ParsedMessage :: Invalid {
1214+ reason :
1215+ InvalidReason :: RequiredFieldMissing {
1216+ tag,
1217+ group_tag : Some ( g) ,
1218+ } ,
1219+ ..
1220+ } => {
1221+ assert_eq ! ( tag, missing_tag) ;
1222+ assert_eq ! ( g, group_tag) ;
1223+ }
1224+ other => panic ! (
1225+ "expected RequiredFieldMissing(tag={missing_tag}, group_tag={group_tag}); \
1226+ msg_type={msg_type}, delimiter={delimiter_tag}, got: {}",
1227+ match & other {
1228+ ParsedMessage :: Valid ( _) => "Valid" . to_string( ) ,
1229+ ParsedMessage :: Invalid { reason, .. } => match reason {
1230+ InvalidReason :: InvalidField ( t) => format!( "InvalidField({t})" ) ,
1231+ InvalidReason :: InvalidGroup ( t) => format!( "InvalidGroup({t})" ) ,
1232+ InvalidReason :: InvalidOrderInGroup { tag, group_tag } => {
1233+ format!( "InvalidOrderInGroup(tag={tag}, group_tag={group_tag})" )
1234+ }
1235+ InvalidReason :: InvalidComponent ( s) => format!( "InvalidComponent({s})" ) ,
1236+ InvalidReason :: InvalidMsgType ( s) => format!( "InvalidMsgType({s})" ) ,
1237+ InvalidReason :: RequiredFieldMissing { tag, group_tag } => {
1238+ format!( "RequiredFieldMissing(tag={tag}, group_tag={group_tag:?})" )
1239+ }
1240+ } ,
1241+ ParsedMessage :: Garbled ( _) => "Garbled" . to_string( ) ,
1242+ ParsedMessage :: UnexpectedError ( _) => "UnexpectedError" . to_string( ) ,
1243+ }
1244+ ) ,
1245+ }
1246+ }
11471247}
0 commit comments