Skip to content

Commit

Permalink
required tags validation in nested components (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
lumber1000 authored Sep 12, 2024
1 parent ee2202c commit b3f3b32
Show file tree
Hide file tree
Showing 4 changed files with 430 additions and 111 deletions.
7 changes: 0 additions & 7 deletions .gitlab-ci.yml

This file was deleted.

70 changes: 52 additions & 18 deletions src/main/kotlin/com/exactpro/th2/codec/fixng/FixNgCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,12 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings)
targetMap[name] = decodedValue
}

private val prereadHeaderFields = arrayOf("BeginString", "BodyLength", "MsgType")
private val prereadHeaderTags = arrayOf(8 /* BeginString */, 9 /* BodyLength */, 35 /* MsgType */)

private fun Message.decode(source: ByteBuf, bodyDef: Message, isDirty: Boolean, dictionaryFields: Map<Int, Field>, context: IReportingContext): MutableMap<String, Any> = mutableMapOf<String, Any>().also { map ->
val tagsSet: MutableSet<Int> = hashSetOf()
val tagsSet: MutableSet<Int> = hashSetOf(*prereadHeaderTags)
val usedComponents = mutableSetOf<String>()

source.forEachField(charset, isDirty) { tag, value ->
val field = get(tag) ?: if (isDirty) {
when (this) {
Expand All @@ -232,21 +234,23 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings)
return@forEachField false
}

usedComponents.addAll(field.path)

field.decode(source, map, tagsSet, value, tag, isDirty, context)
return@forEachField true
}

for (field in fields.values) {
if (!field.isRequired) continue

val tag = when (field) {
is Primitive -> field.tag
is Group -> field.counter
else -> error("Only `Primitive` and `Group` fields expected to be `required`")
}
validateRequiredTags(requiredTags, tagsSet, isDirty, context)
for (componentName in usedComponents) {
val requiredTags = conditionallyRequiredTags[componentName] ?: continue
validateRequiredTags(requiredTags, tagsSet, isDirty, context)
}
}

if (!tagsSet.contains(tag) && field.name !in prereadHeaderFields) {
handleError(isDirty, context, "Required field missing. Field name: ${field.name}.")
private fun validateRequiredTags(requiredTags: Set<Int>, tagsSet: Set<Int>, isDirty: Boolean, context: IReportingContext) {
for (tag in requiredTags) {
if (!tagsSet.contains(tag)) {
handleError(isDirty, context, "Required tag missing. Tag: $tag.")
}
}
}
Expand Down Expand Up @@ -363,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) {
Expand Down Expand Up @@ -480,6 +484,8 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings)
override val path: List<String>,
val type: String,
override val fields: Map<String, Field>,
val requiredTags: Set<Int>,
val conditionallyRequiredTags: Map<String, Set<Int>>
) : Field, FieldMap()

data class Group(
Expand Down Expand Up @@ -565,7 +571,7 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings)
fields.forEach { (name, field) ->
when {
field !is IMessageStructure -> this[name] = field.toPrimitive(path, isForEncode || isRequiredParent)
field.isGroup -> this[name] = field.toGroup(isForEncode, path, isForEncode || isRequiredParent)
field.isGroup -> this[name] = field.toGroup(isForEncode, path)
field.isComponent -> if (isForEncode) {
this[name] = field.toMessage(true, path + name)
} else {
Expand All @@ -579,31 +585,59 @@ class FixNgCodec(dictionary: IDictionaryStructure, settings: FixNgCodecSettings)
fields.values.forEach { field ->
when {
field !is IMessageStructure -> this[field.tag] = field.toPrimitive(path, true)
field.isGroup -> this[field.tag] = field.toGroup(false, path, true)
field.isGroup -> this[field.tag] = field.toGroup(false, path)
field.isComponent -> this += convertToFieldsByTag(field.fields, path + field.name)
}
}
}

private fun collectRequiredTags(fields: Map<String, IFieldStructure>, target: MutableSet<Int>): Set<Int> {
for (field in fields.values) {
when {
!field.isRequired -> continue
field !is IMessageStructure -> target.add(field.tag)
field.isGroup -> target.add(field.tag)
field.isComponent -> collectRequiredTags(field.fields, target)
}
}
return target
}

private fun collectConditionallyRequiredTags(fields: Map<String, IFieldStructure>, target: MutableMap<String, Set<Int>>): Map<String, Set<Int>> {
for (field in fields.values) {
if (field is IMessageStructure && field.isComponent) {
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())
}
}
}
return target
}

private fun IMessageStructure.toMessage(isForEncode: Boolean, path: List<String>): Message = Message(
name = name,
type = StructureUtils.getAttributeValue(this, FIELD_MESSAGE_TYPE) ?: name,
fields = convertToFieldsByName(this.fields, isForEncode, path, !isComponent || isRequired),
path = path,
isRequired = isRequired
isRequired = isRequired,
requiredTags = if (isForEncode) emptySet() else collectRequiredTags(fields, mutableSetOf()),
conditionallyRequiredTags = if (isForEncode) emptyMap() else collectConditionallyRequiredTags(fields, mutableMapOf())
)

private fun getFirstTag(message: IMessageStructure): Int = message.fields.values.first().let {
if (it is IMessageStructure && it.isComponent) getFirstTag(it) else it.tag
}

private fun IMessageStructure.toGroup(isForEncode: Boolean, path: List<String>, isRequiredParent: Boolean): Group = Group(
private fun IMessageStructure.toGroup(isForEncode: Boolean, path: List<String>): Group = Group(
name = name,
counter = tag,
delimiter = getFirstTag(this),
fields = convertToFieldsByName(this.fields, isForEncode, emptyList(), true),
path = path,
isRequired = isRequiredParent && isRequired
isRequired = isRequired
)

fun IDictionaryStructure.toMessages(isForEncode: Boolean): List<Message> = messages.values
Expand Down
Loading

0 comments on commit b3f3b32

Please sign in to comment.