diff --git a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt index c39a0df..bb044be 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt @@ -250,7 +250,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) private fun validateRequiredTags(requiredTags: Set, tagsSet: Set, isDirty: Boolean, context: IReportingContext) { for (tag in requiredTags) { if (!tagsSet.contains(tag)) { - handleError(isDirty, context, "Required tag missing. Tag: ${tag}.") + handleError(isDirty, context, "Required tag missing. Tag: $tag.") } } } @@ -367,7 +367,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) handleError(isDirty, context, "Wrong date/time value in ${field.primitiveType.name} field '$field.name'. Value: $value.", value) } } - else -> handleError(isDirty, context, "Wrong type value in field ${field.name}. Actual: ${value.javaClass} (value: $value). Expected ${field.primitiveType}") + else -> handleError(isDirty, context, "Wrong type value in field ${field.name}. Actual: ${value.javaClass} (value: $value). Expected ${field.primitiveType}", value) } val stringValue = when (valueToEncode) { @@ -603,16 +603,15 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) return target } - private fun collectConditionallyRequiredTags(fields: Map, isParentRequired: Boolean, target: MutableMap>): Map> { + private fun collectConditionallyRequiredTags(fields: Map, target: MutableMap>): Map> { for (field in fields.values) { if (field is IMessageStructure && field.isComponent) { - val isCurrentRequired = isParentRequired && field.isRequired + val isCurrentRequired = field.isRequired // There is no point in adding tags from optional components that contain only one field // (such a field is effectively optional even if it has a required flag). if (!isCurrentRequired && field.fields.size > 1) { target[field.name] = collectRequiredTags(field.fields, mutableSetOf()) } - collectConditionallyRequiredTags(field.fields, isCurrentRequired, target) } } return target @@ -625,7 +624,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings) path = path, isRequired = isRequired, requiredTags = if (isForEncode) emptySet() else collectRequiredTags(fields, mutableSetOf()), - conditionallyRequiredTags = if (isForEncode) emptyMap() else collectConditionallyRequiredTags(fields, true, mutableMapOf()) + conditionallyRequiredTags = if (isForEncode) emptyMap() else collectConditionallyRequiredTags(fields, mutableMapOf()) ) private fun getFirstTag(message: IMessageStructure): Int = message.fields.values.first().let { diff --git a/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt index 1c4503b..6c9dcd7 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/fixng/FixNgCodecTest.kt @@ -233,6 +233,76 @@ class FixNgCodecTest { fun `tag appears out of order`() = decodeTest(MSG_TAG_OUT_OF_ORDER, "Tag appears out of order: 999", dirtyMode = false) + @Test + fun `decode nested components`() = + decodeTest(MSG_NESTED_REQ_COMPONENTS, expectedMessage = parsedMessageWithNestedComponents) + + @Test + fun `decode with missing req field in req nested component`() { + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]?.remove("OrdType") + decodeTest(MSG_NESTED_REQ_COMPONENTS_MISSED_REQ, expectedErrorText = "Required tag missing. Tag: 40.", expectedMessage = parsedMessageWithNestedComponents) + } + + @Test + fun `decode with missing optional field in req nested component`() { + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]?.remove("Text") + decodeTest(MSG_NESTED_REQ_COMPONENTS_MISSED_OPTIONAL, expectedMessage = parsedMessageWithNestedComponents) + } + + private fun convertToOptionalComponent(): ParsedMessage { + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["header"] as MutableMap)["MsgType"] = "TEST_2" + val msgBuilder = parsedMessageWithNestedComponents.toBuilder() + msgBuilder.setType("NestedOptionalComponentTestMessage") + return msgBuilder.build() + } + + @Test + fun `decode nested optional components`() { + val message = convertToOptionalComponent() + decodeTest(MSG_NESTED_OPT_COMPONENTS, expectedMessage = message) + } + + @Test + fun `decode with missing req field in opt nested component`() { + val message = convertToOptionalComponent() + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]?.remove("OrdType") + decodeTest(MSG_NESTED_OPT_COMPONENTS_MISSED_REQ, expectedErrorText = "Required tag missing. Tag: 40.", expectedMessage = message) + } + + @Test + fun `decode with missing all fields in opt nested component`() { + val message = convertToOptionalComponent() + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>).remove("InnerComponent") + decodeTest(MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS, expectedErrorText = "Required tag missing. Tag: 40.", expectedMessage = message) + } + + @Test + fun `decode with missing all fields in inner and outer nested components`() { + val message = convertToOptionalComponent() + parsedBodyWithNestedComponents.remove("OuterComponent") + decodeTest(MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS_INNER_AND_OUTER, expectedMessage = message) + } + + @Test + fun `decode with missing req fields in both inner and outer components`() { + val message = convertToOptionalComponent() + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>).remove("LeavesQty") + @Suppress("UNCHECKED_CAST") + (parsedBodyWithNestedComponents["OuterComponent"] as MutableMap>)["InnerComponent"]!!.remove("OrdType") + decodeTest( + MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_OUTER_FIELDS_AND_REQ_INNER_FIELD, + expectedErrorText = "Required tag missing. Tag: 40.", + expectedSecondErrorText = "Required tag missing. Tag: 151.", + expectedMessage = message + ) + } + private fun encodeTest( expectedRawMessage: String, expectedWarning: String? = null, @@ -258,6 +328,7 @@ class FixNgCodecTest { private fun decodeTest( rawMessageString: String, expectedErrorText: String? = null, + expectedSecondErrorText: String? = null, expectedMessage: ParsedMessage = parsedMessage, dirtyMode: Boolean = true, decodeToStringValues: Boolean = false @@ -296,7 +367,13 @@ class FixNgCodecTest { if (expectedErrorText == null) { assertThat(reportingContext.warnings).isEmpty() } else { - assertThat(reportingContext.warnings.single()).startsWith(DIRTY_MODE_WARNING_PREFIX + expectedErrorText) + if (expectedSecondErrorText == null) { + assertThat(reportingContext.warnings.single()).startsWith(DIRTY_MODE_WARNING_PREFIX + expectedErrorText) + } else { + assertThat(reportingContext.warnings).size().isEqualTo(2) + assertThat(reportingContext.warnings[0]).startsWith(DIRTY_MODE_WARNING_PREFIX + expectedErrorText) + assertThat(reportingContext.warnings[1]).startsWith(DIRTY_MODE_WARNING_PREFIX + expectedSecondErrorText) + } } } @@ -385,6 +462,35 @@ class FixNgCodecTest { ) ) + private val parsedMessageWithNestedComponents = ParsedMessage( + MessageId("test_alias", Direction.OUTGOING, 0L, Instant.now(), emptyList()), + EventId("test_id", "test_book", "test_scope", Instant.now()), + "NestedRequiredComponentTestMessage", + mutableMapOf("encode-mode" to "dirty"), + PROTOCOL, + mutableMapOf( + "header" to mutableMapOf( + "BeginString" to "FIXT.1.1", + "BodyLength" to 59, + "MsgType" to "TEST_1", + "MsgSeqNum" to 125, + "TargetCompID" to "INET", + "SenderCompID" to "MZHOT0" + ), + "OuterComponent" to mutableMapOf( + "LeavesQty" to BigDecimal(1234), // tag 151 + "InnerComponent" to mutableMapOf( + "Text" to "text_1", // tag 58 + "OrdType" to '1' // 40 + ) + ), + "trailer" to mapOf( + "CheckSum" to "191" + ) + ) + ) + private val parsedBodyWithNestedComponents: MutableMap = parsedMessageWithNestedComponents.body as MutableMap + companion object { private const val DIRTY_MODE_WARNING_PREFIX = "Dirty mode WARNING: " @@ -402,5 +508,15 @@ class FixNgCodecTest { private const val MSG_NON_PRINTABLE = "8=FIXT.1.1\u00019=303\u000135=8\u000149=SENDER\u000156=RECEIVER\u000134=10947\u000152=20230419-10:36:07.415088\u000117=495504662\u000111=zSuNbrBIZyVljs\u000141=zSuNbrBIZyVljs\u000137=49415882\u0001150=0\u000139=0\u0001151=500\u000114=500\u000148=NWDR\u000122=8\u0001453=2\u0001448=NGALL1FX01\u0001447=D\u0001452=76\u0001448=0\u0001447=P\u0001452=3\u00011=test\taccount\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=171\u0001" private const val MSG_REQUIRED_HEADER_REMOVED = "8=FIXT.1.1\u00019=236\u000135=8\u000117=495504662\u000111=zSuNbrBIZyVljs\u000141=zSuNbrBIZyVljs\u000137=49415882\u0001150=0\u000139=0\u0001151=500\u000114=500\u000148=NWDR\u000122=8\u0001453=2\u0001448=NGALL1FX01\u0001447=D\u0001452=76\u0001448=0\u0001447=P\u0001452=3\u00011=test\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=050\u0001" private const val MSG_TAG_OUT_OF_ORDER = "8=FIXT.1.1\u00019=295\u000135=8\u000149=SENDER\u000156=RECEIVER\u000134=10947\u000152=20230419-10:36:07.415088\u000117=495504662\u000111=zSuNbrBIZyVljs\u000141=zSuNbrBIZyVljs\u000137=49415882\u0001150=0\u000139=0\u0001151=500\u000114=500\u000148=NWDR\u000122=8\u0001453=2\u0001448=NGALL1FX01\u0001447=D\u0001452=76\u0001448=0\u0001447=P\u0001452=3\u00011=test\u000140=A\u000159=0\u000154=B\u000155=ABC\u000138=500\u000144=1000\u000147=500\u000160=20180205-10:38:08.000008\u000110=000\u0001999=500\u0001" + + private const val MSG_NESTED_REQ_COMPONENTS = "8=FIXT.1.19=5935=TEST_149=MZHOT056=INET34=12558=text_140=1151=123410=191" + private const val MSG_NESTED_REQ_COMPONENTS_MISSED_REQ = "8=FIXT.1.19=5935=TEST_149=MZHOT056=INET34=12558=text_1151=123410=191" + private const val MSG_NESTED_REQ_COMPONENTS_MISSED_OPTIONAL = "8=FIXT.1.19=5935=TEST_149=MZHOT056=INET34=12540=1151=123410=191" + + private const val MSG_NESTED_OPT_COMPONENTS = "8=FIXT.1.19=5935=TEST_249=MZHOT056=INET34=12558=text_140=1151=123410=191" + private const val MSG_NESTED_OPT_COMPONENTS_MISSED_REQ = "8=FIXT.1.19=5935=TEST_249=MZHOT056=INET34=12558=text_1151=123410=191" + private const val MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS = "8=FIXT.1.19=5935=TEST_249=MZHOT056=INET34=125151=123410=191" + private const val MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_FIELDS_INNER_AND_OUTER = "8=FIXT.1.19=5935=TEST_249=MZHOT056=INET34=12510=191" + private const val MSG_NESTED_OPT_COMPONENTS_MISSED_ALL_OUTER_FIELDS_AND_REQ_INNER_FIELD = "8=FIXT.1.19=5935=TEST_249=MZHOT056=INET34=12558=text_110=191" } } \ No newline at end of file diff --git a/src/test/resources/dictionary.xml b/src/test/resources/dictionary.xml index a04f185..a1f9bd2 100644 --- a/src/test/resources/dictionary.xml +++ b/src/test/resources/dictionary.xml @@ -7361,6 +7361,28 @@ + + Message + false + TEST_1 + + + + Message + false + TEST_2 + + + + Component + + + + + Component + + + Component