diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt new file mode 100644 index 0000000000..aea4f448ed --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt @@ -0,0 +1,6 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +class AdditionalPropsDto( + val value: String, + val source: String? = null +) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt new file mode 100644 index 0000000000..e96aa49a8e --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt @@ -0,0 +1,14 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +import com.fasterxml.jackson.annotation.JsonAnySetter + +class AdditionalPropsInlineDto(val stringProp: String) { + + val additional: MutableMap = mutableMapOf() + + @JsonAnySetter + fun addAdditional(key: String, value: AdditionalPropsDto) { + additional[key] = value + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt new file mode 100644 index 0000000000..cfe1596f54 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt @@ -0,0 +1,14 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +import com.fasterxml.jackson.annotation.JsonAnySetter + +class AdditionalPropsNoRootDto() { + + val additional: MutableMap = mutableMapOf() + + @JsonAnySetter + fun addAdditional(key: String, value: AdditionalPropsDto) { + additional[key] = value + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt new file mode 100644 index 0000000000..b0a1c31146 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt @@ -0,0 +1,14 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +import com.fasterxml.jackson.annotation.JsonAnySetter + +class AdditionalPropsRefDto(val stringProp: String) { + + val additional: MutableMap = mutableMapOf() + + @JsonAnySetter + fun addAdditional(key: String, value: ChildSchemaDto) { + additional[key] = value + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt index 8c41fefab4..3b5f06c2ca 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt @@ -54,4 +54,19 @@ class DtoReflectiveAssertRest { return ResponseEntity.ok("OK") } + @PostMapping(path = ["/additional-properties-inline"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + open fun additionalPropertiesInline(@RequestBody body: AdditionalPropsInlineDto) : ResponseEntity{ + return ResponseEntity.ok("OK") + } + + @PostMapping(path = ["/additional-properties-ref"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + open fun additionalPropertiesRef(@RequestBody body: AdditionalPropsRefDto) : ResponseEntity{ + return ResponseEntity.ok("OK") + } + + @PostMapping(path = ["/additional-properties-no-root"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + open fun additionalPropertiesNoRoot(@RequestBody body: AdditionalPropsNoRootDto) : ResponseEntity{ + return ResponseEntity.ok("OK") + } + } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml index f2367d8142..e975247445 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml @@ -190,6 +190,73 @@ paths: responses: '200': description: OK + /additional-properties-inline: + post: + summary: Update inline additionalProperties + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + stringProp: + type: string + additionalProperties: + type: object + required: + - value + properties: + value: + type: string + source: + type: string + required: + - stringProp + responses: + '200': + description: OK + /additional-properties-ref: + post: + summary: Update ref additionalProperties + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + stringProp: + type: string + additionalProperties: + $ref: '#/components/schemas/ChildSchema' + required: + - stringProp + responses: + '204': + description: Attributes updated + /additional-properties-no-root: + post: + summary: Update ref additionalProperties + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: + type: object + required: + - value + - source + properties: + value: + type: string + source: + type: string + responses: + '204': + description: Attributes updated components: schemas: diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt index 5ffa8d1f97..accd0aeb47 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt @@ -10,8 +10,10 @@ import org.junit.jupiter.api.Test import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 import kotlin.reflect.full.createInstance +import kotlin.reflect.full.memberFunctions import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaMethod class DtoReflectiveAssertEMTest: SpringTestBase() { @@ -45,6 +47,9 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/items-components", "OK") assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/enum-type", "OK") assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/enum-examples", "OK") + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-inline", "OK") + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-ref", "OK") + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-no-root", "OK") } assertPrimitiveTypeDtoCreated() @@ -55,6 +60,9 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertOneOfDtoCreated() assertEnumTypeDtoCreated() assertEnumExampleDtoCreated() + assertAdditionalPropertiesInlineDtoCreated() + assertAdditionalPropertiesRefDtoCreated() + assertAdditionalPropertiesNoRootDtoCreated() } private fun assertPrimitiveTypeDtoCreated() { @@ -74,9 +82,9 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { private fun assertParentAndChildDtosCreated() { val (parentKlass, parentInstance) = initDtoClass("ParentSchema") - val (childKass, childInstance) = initDtoClass("ChildSchema") - assertProperty(childKass, childInstance, "name", "Philip") - assertProperty(childKass, childInstance, "age", 31) + val (childKlass, childInstance) = initDtoClass("ChildSchema") + assertProperty(childKlass, childInstance, "name", "Philip") + assertProperty(childKlass, childInstance, "age", 31) assertProperty(parentKlass, parentInstance, "label", "EM_TEST") assertProperty(parentKlass, parentInstance, "child", childInstance) } @@ -127,6 +135,31 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertProperty(klass, instance, "listValue", mutableListOf("s1", "s2", "s3")) } + private fun assertAdditionalPropertiesInlineDtoCreated() { + val (parentKlass, parentInstance) = initDtoClass("POST__additional_properties_inline") + assertProperty(parentKlass, parentInstance, "stringProp", "A text value") + val (childKlass, childInstance) = initDtoClass("POST__additional_properties_inline_ap") + assertProperty(childKlass, childInstance, "value", "My value") + assertAdditionalPropertiesFunction(parentKlass, parentInstance, "aRandomKey", childInstance) + } + + private fun assertAdditionalPropertiesRefDtoCreated() { + val (parentKlass, parentInstance) = initDtoClass("POST__additional_properties_ref") + assertProperty(parentKlass, parentInstance, "stringProp", "A text value") + val (childKlass, childInstance) = initDtoClass("ChildSchema") + assertProperty(childKlass, childInstance, "name", "Philip") + assertProperty(childKlass, childInstance, "age", 31) + assertAdditionalPropertiesFunction(parentKlass, parentInstance, "anotherRandomKey", childInstance) + } + + private fun assertAdditionalPropertiesNoRootDtoCreated() { + val (parentKlass, parentInstance) = initDtoClass("POST__additional_properties_no_root") + val (childKlass, childInstance) = initDtoClass("POST__additional_properties_no_root_ap") + assertProperty(childKlass, childInstance, "value", "WebFuzzing") + assertProperty(childKlass, childInstance, "source", "Dataset") + assertAdditionalPropertiesFunction(parentKlass, parentInstance, "no_root_key", childInstance) + } + private fun initDtoClass(name: String): Pair, Any> { val className = ClassName("org.foo.dto.$name") val klass = loadClass(className).kotlin @@ -146,4 +179,22 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { Assertions.assertEquals(propertyValue, property?.get(instance)) } + private fun assertAdditionalPropertiesFunction(klass: KClass, instance: Any, propertyName: String, propertyValue: Any?) { + val setterFunction = klass.memberFunctions.firstOrNull { + it.name == "addAdditionalProperty" + }?.javaMethod + Assertions.assertNotNull(setterFunction) + setterFunction?.isAccessible = true + setterFunction?.invoke(instance, propertyName, propertyValue) + + val getterFunction = klass.memberFunctions.firstOrNull { + it.name == "getAdditionalProperties" + }?.javaMethod + Assertions.assertNotNull(getterFunction) + getterFunction?.isAccessible = true + val map = getterFunction?.invoke(instance) as MutableMap<*, *> + Assertions.assertNotNull(map) + Assertions.assertEquals(propertyValue, map[propertyName]) + } + } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt index 66e380ee89..09c68c38be 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt @@ -2,10 +2,18 @@ package org.evomaster.core.output.dto class DtoClass( val name: String, - val fields: MutableList = mutableListOf()) { + val fieldsMap: MutableMap = mutableMapOf(), + var additionalPropertiesDtoName: String? = null +) { - fun addField(field: DtoField) { - if (field !in fields) fields.add(field) + fun addField(fieldName: String, field: DtoField) { + if (!fieldsMap.containsKey(fieldName)) { + fieldsMap[fieldName] = field + } + } + + fun hasAdditionalProperties(): Boolean { + return !additionalPropertiesDtoName.isNullOrEmpty() } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt index 215a6ec04e..05bada0087 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt @@ -53,4 +53,13 @@ interface DtoOutput { */ fun getAddElementToListStatement(listVarName: String, value: String): String + /** + * @param additionalPropertiesVarName variable in which the additionalProperties will be added + * @param key for the additional property to be added to the DTO + * @param value variable name representing the additional properties + * + * @return the add additional properties statement + */ + fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String + } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index fa61320f4f..64d229d788 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -12,6 +12,8 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.collection.FixedMapGene +import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -19,10 +21,13 @@ import org.evomaster.core.search.gene.numeric.DoubleGene import org.evomaster.core.search.gene.numeric.FloatGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.numeric.LongGene +import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.gene.utils.GeneUtils.isInactiveOptionalGene import org.evomaster.core.search.gene.wrapper.ChoiceGene +import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.utils.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -85,23 +90,22 @@ class DtoWriter( private fun calculateDtoFromChoice(gene: ChoiceGene<*>, actionName: String) { if (hasObjectOrArrayGene(gene)) { val dtoName = TestWriterUtils.safeVariableName(actionName) - if (!dtoCollector.contains(dtoName)) { - val dtoClass = DtoClass(dtoName) - val children = gene.getViewOfChildren() - // merge options into a single DTO - children.forEach { childGene -> - when (childGene) { - is ObjectGene -> populateDtoClass(dtoClass, childGene) - is ArrayGene<*> -> { - val template = childGene.template - if (template is ObjectGene) { - populateDtoClass(dtoClass, template) - } + + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } + val children = gene.getViewOfChildren() + // merge options into a single DTO + children.forEach { childGene -> + when (childGene) { + is ObjectGene -> populateDtoClass(dtoClass, childGene) + is ArrayGene<*> -> { + val template = childGene.template + if (template is ObjectGene) { + populateDtoClass(dtoClass, template) } } } - dtoCollector.put(dtoName, dtoClass) } + dtoCollector[dtoName] = dtoClass } } @@ -113,6 +117,7 @@ class DtoWriter( when { gene is ObjectGene -> calculateDtoFromObject(gene, actionName) gene is ArrayGene<*> -> calculateDtoFromArray(gene, actionName) + gene is FixedMapGene<*, *> -> calculateDtoFromFixedMapGene(gene, actionName) isPrimitiveGene(gene) -> return else -> { throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $actionName") @@ -120,6 +125,16 @@ class DtoWriter( } } + private fun calculateDtoFromFixedMapGene(gene: FixedMapGene<*, *>, actionName: String) { + val dtoName = TestWriterUtils.safeVariableName(actionName) + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } + val additionalProperties = gene.getViewOfChildren()!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } as List> + addAdditionalProperties(dtoClass, additionalProperties) + dtoCollector[dtoName] = dtoClass + } + private fun isPrimitiveGene(gene: Gene): Boolean { return when (gene) { is StringGene, is IntegerGene, is LongGene, is DoubleGene, is FloatGene, is BooleanGene -> true @@ -130,11 +145,9 @@ class DtoWriter( private fun calculateDtoFromObject(gene: ObjectGene, actionName: String) { // TODO: Determine strategy for objects that are not defined as a component and do not have a name val dtoName = TestWriterUtils.safeVariableName(gene.refType?:actionName) - if (!dtoCollector.contains(dtoName)) { - val dtoClass = DtoClass(dtoName) - populateDtoClass(dtoClass, gene) - dtoCollector.put(dtoName, dtoClass) - } + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } + populateDtoClass(dtoClass, gene) + dtoCollector[dtoName] = dtoClass } private fun calculateDtoFromArray(gene: ArrayGene<*>, actionName: String) { @@ -149,12 +162,16 @@ class DtoWriter( } private fun populateDtoClass(dtoClass: DtoClass, gene: ObjectGene) { - gene.fixedFields.forEach { field -> + val includedFields = gene.fixedFields.filter { + it !is CycleObjectGene && (it !is OptionalGene || (it.isActive && it.gene !is CycleObjectGene)) + } .filter { it.isPrintable() } + + includedFields.forEach { field -> try { val wrappedGene = field.getLeafGene() val dtoField = getDtoField(field.name, wrappedGene) - dtoClass.addField(dtoField) - if (wrappedGene is ObjectGene && !dtoCollector.contains(dtoField.type)) { + dtoClass.addField(field.name, dtoField) + if (wrappedGene is ObjectGene) { calculateDtoFromObject(wrappedGene, dtoField.type) } if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { @@ -168,6 +185,40 @@ class DtoWriter( assert(false) } } + if (!gene.isFixed) { + val additionalFields = gene.additionalFields!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } as List> + addAdditionalProperties(dtoClass, additionalFields) + } + } + + private fun addAdditionalProperties(dtoClass: DtoClass, additionalProperties: List>) { + if (additionalProperties.isEmpty()) { + return + } + additionalProperties.forEach { field -> + try { + val wrappedGene = (field as PairGene).second.getLeafGene() + if (wrappedGene is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene, additionalPropertiesDtoName) + } + if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.template.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene.template, additionalPropertiesDtoName) + } + } catch (ex: Exception) { + log.warn( + "A failure has occurred when collecting DTO additional properties. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) + } + } } private fun getDtoField(fieldName: String, field: Gene?): DtoField { diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 91d78d73bd..c4f81f38f6 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -7,6 +7,8 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.collection.FixedMapGene +import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -18,9 +20,12 @@ import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.gene.utils.GeneUtils.isInactiveOptionalGene import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.utils.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory /** * Provides a mapping between a Gene and its DTO representation at use. Takes in the [OutputFormat] to delegate @@ -30,6 +35,8 @@ class GeneToDto( val outputFormat: OutputFormat ) { + private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java) + private var dtoOutput: DtoOutput = if (outputFormat.isJava()) { JavaDtoOutput() } else if (outputFormat.isKotlin()){ @@ -54,10 +61,11 @@ class GeneToDto( TestWriterUtils.safeVariableName(template.refType?:fallback) } else { // TODO handle arrays of basic data types - return getListType(fallback, template, capitalize) + getListType(fallback, template, capitalize) } } is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback) + is FixedMapGene<*,*> -> TestWriterUtils.safeVariableName(fallback) else -> throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $fallback") } } @@ -75,6 +83,7 @@ class GeneToDto( is ObjectGene -> getObjectDtoCall(gene, dtoName, counters) is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counters, null, capitalize) is ChoiceGene<*> -> getDtoCall(gene.activeGene(), dtoName, counters, capitalize) + is FixedMapGene<*,*> -> getFixedMapGeneDtoCall(gene, dtoName, counters) else -> throw RuntimeException("BUG: Gene $gene (with type ${this::class.java.simpleName}) should not be creating DTOs") } } @@ -85,7 +94,7 @@ class GeneToDto( val result = mutableListOf() result.add(dtoOutput.getNewObjectStatement(dtoName, dtoVarName)) - val includedFields = gene.fields.filter { + val includedFields = gene.fixedFields.filter { it !is CycleObjectGene && (it !is OptionalGene || (it.isActive && it.gene !is CycleObjectGene)) } .filter { it.isPrintable() } @@ -118,9 +127,87 @@ class GeneToDto( } } + if (!gene.isFixed) { + val additionalProperties = gene.additionalFields!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } as List> + setAdditionalProperties(dtoName, dtoVarName, result, counters, additionalProperties) + } + return DtoCall(dtoVarName, result) } + private fun getFixedMapGeneDtoCall(gene: FixedMapGene<*, *>, dtoName: String, counters: MutableList): DtoCall { + val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}" + val result = mutableListOf() + result.add(dtoOutput.getNewObjectStatement(dtoName, dtoVarName)) + val additionalProperties = gene.getViewOfChildren()!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } as List> + setAdditionalProperties(dtoName, dtoVarName, result, counters, additionalProperties) + return DtoCall(dtoVarName, result) + } + + private fun setAdditionalProperties(dtoName: String, dtoVarName: String, result: MutableList, counters: MutableList, additionalProperties: List>) { + if (additionalProperties.isEmpty()) { + return + } + var additionalPropertiesCounter = 1 + additionalProperties.forEach { field -> + try { + val childCounter = mutableListOf() + childCounter.addAll(counters) + childCounter.add(additionalPropertiesCounter++) + val key = (field as PairGene).first.getLeafGene() + .getValueAsPrintableString(targetFormat = outputFormat) + val leafGene = (field as PairGene).second.getLeafGene() + val additionalPropertiesVarName = when (leafGene) { + is ObjectGene -> { + val attributeName = leafGene.refType ?: "${dtoName}_ap" + val childDtoCall = + getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, true) + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName + } + + is ArrayGene<*> -> { + val attributeName = if (leafGene.template is ObjectGene) { + leafGene.template.refType ?: "${dtoName}_ap" + } else { + getListType("", leafGene.template, true) + } + val childDtoCall = getArrayDtoCall( + leafGene, + getDtoName(leafGene, attributeName, true), + childCounter, + attributeName, + true + ) + + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName + } + + else -> throw IllegalStateException("Additional properties should only be Map related genes") + } + result.add( + dtoOutput.getAddElementToAdditionalPropertiesStatement( + dtoVarName, + key, + additionalPropertiesVarName + ) + ) + } catch (ex: Exception) { + log.warn( + "A failure has occurred when writing DTOs. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) + } + } + } + private fun getArrayDtoCall(gene: ArrayGene<*>, dtoName: String, counters: MutableList, targetAttribute: String?, capitalize: Boolean): DtoCall { val result = mutableListOf() val template = gene.template diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt index cafcc6fc62..f5ad648d3d 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt @@ -3,7 +3,7 @@ package org.evomaster.core.output.dto import org.evomaster.core.output.Lines import org.evomaster.core.output.OutputFormat import org.evomaster.core.output.TestSuiteFileName -import org.evomaster.core.utils.StringUtils +import org.evomaster.core.utils.StringUtils.capitalizeFirstChar import java.nio.file.Path class JavaDtoOutput: JvmDtoOutput() { @@ -24,7 +24,7 @@ class JavaDtoOutput: JvmDtoOutput() { } override fun getSetterStatement(dtoVarName: String, attributeName: String, value: String): String { - return "$dtoVarName.set${StringUtils.capitalization(attributeName)}($value);" + return "$dtoVarName.set${capitalizeFirstChar(attributeName)}($value);" } override fun getNewListStatement(listType: String, listVarName: String): String { @@ -35,6 +35,10 @@ class JavaDtoOutput: JvmDtoOutput() { return "$listVarName.add($value);" } + override fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String { + return "$additionalPropertiesVarName.addAdditionalProperty($key, $value);" + } + private fun initClass(lines: Lines, dtoFilename: String) { lines.add("@JsonInclude(JsonInclude.Include.NON_NULL)") lines.add("public class $dtoFilename {") @@ -51,20 +55,32 @@ class JavaDtoOutput: JvmDtoOutput() { } private fun addVariables(lines: Lines, dtoClass: DtoClass) { - dtoClass.fields.forEach { + dtoClass.fieldsMap.forEach { lines.indented { - lines.add("@JsonProperty(\"${it.name}\")") - lines.add("private Optional<${it.type}> ${it.name};") + lines.add("@JsonProperty(\"${it.key}\")") + lines.add("private Optional<${it.value.type}> ${it.key};") + } + lines.addEmpty() + } + if (dtoClass.hasAdditionalProperties()) { + lines.indented { + /* + * We ignore additionalProperties map since otherwise Jackson will attempt to serialize it as + * { ..., "additionalProperties": { ... } } + * Where actually what we need is the inner object with the different key-values. + */ + lines.add("@JsonIgnore") + lines.add("private Map additionalProperties = new HashMap<>();") } lines.addEmpty() } } private fun addGettersAndSetters(lines: Lines, dtoClass: DtoClass) { - dtoClass.fields.forEach { - val varName = it.name - val varType = it.type - val capitalizedVarName = StringUtils.capitalization(varName) + dtoClass.fieldsMap.forEach { + val varName = it.key + val varType = it.value.type + val capitalizedVarName = capitalizeFirstChar(varName) lines.indented { lines.add("public Optional<${varType}> get${capitalizedVarName}() {") lines.indented { @@ -80,6 +96,26 @@ class JavaDtoOutput: JvmDtoOutput() { } lines.addEmpty() } + if (dtoClass.hasAdditionalProperties()) { + lines.indented { + // Ensures that entries stored in additionalProperties are flattened into the JSON object serialization. + lines.add("@JsonAnyGetter") + lines.add("public Map getAdditionalProperties() {") + lines.indented { + lines.add("return additionalProperties;") + } + lines.add("}") + lines.addEmpty() + // Allows the DTO to accept JSON properties that are not declared as explicit fields. + lines.add("@JsonAnySetter") + lines.add("public void addAdditionalProperty(String name, ${dtoClass.additionalPropertiesDtoName} value) {") + lines.indented { + lines.add("this.additionalProperties.put(name, value);") + } + lines.add("}") + } + lines.addEmpty() + } } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt index dc48eff72c..eaa843aaae 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt @@ -15,9 +15,14 @@ abstract class JvmDtoOutput: DtoOutput { } protected fun addImports(lines: Lines) { + lines.addStatement("import java.util.HashMap") lines.addStatement("import java.util.List") + lines.addStatement("import java.util.Map") lines.addStatement("import java.util.Optional") lines.addEmpty() + lines.addStatement("import com.fasterxml.jackson.annotation.JsonAnyGetter") + lines.addStatement("import com.fasterxml.jackson.annotation.JsonAnySetter") + lines.addStatement("import com.fasterxml.jackson.annotation.JsonIgnore") lines.addStatement("import com.fasterxml.jackson.annotation.JsonInclude") lines.addStatement("import com.fasterxml.jackson.annotation.JsonProperty") lines.addEmpty() diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt index 89330ce1fa..33adf142aa 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt @@ -32,21 +32,55 @@ class KotlinDtoOutput: JvmDtoOutput() { return "$listVarName.add($value)" } + override fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String { + return "$additionalPropertiesVarName.addAdditionalProperty($key, $value)" + } + private fun declareClass(lines: Lines, dtoFilename: String, dtoClass: DtoClass) { lines.add("@JsonInclude(JsonInclude.Include.NON_NULL)") - lines.add("class $dtoFilename(") + lines.add("class $dtoFilename {") addVariables(lines, dtoClass) - lines.add(")") + lines.add("}") } private fun addVariables(lines: Lines, dtoClass: DtoClass) { - dtoClass.fields.forEach { + dtoClass.fieldsMap.forEach { lines.indented { - lines.add("@JsonProperty(\"${it.name}\")") - lines.add("var ${it.name}: ${it.type}? = null,") + lines.add("@JsonProperty(\"${it.key}\")") + lines.add("var ${it.key}: ${it.value.type}? = null") } lines.addEmpty() } + if (dtoClass.hasAdditionalProperties()) { + lines.indented { + /* + * We ignore additionalProperties map since otherwise Jackson will attempt to serialize it as + * { ..., "additionalProperties": { ... } } + * Where actually what we need is the inner object with the different key-values. + */ + lines.add("@JsonIgnore") + lines.add("private val additionalProperties: MutableMap = mutableMapOf()") + lines.addEmpty() + + // Ensures that entries stored in additionalProperties are flattened into the JSON object serialization. + lines.add("@JsonAnyGetter") + lines.add("fun getAdditionalProperties(): MutableMap {") + lines.indented { + lines.add("return additionalProperties") + } + lines.add("}") + lines.addEmpty() + + // Allows the DTO to accept JSON properties that are not declared as explicit fields. + lines.add("@JsonAnySetter") + lines.add("fun addAdditionalProperty(name: String, value: ${dtoClass.additionalPropertiesDtoName}) {") + lines.indented { + lines.add("additionalProperties[name] = value") + } + lines.add("}") + lines.addEmpty() + } + } } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt index 75a33ad81a..607d6e9088 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt @@ -28,6 +28,7 @@ import org.evomaster.core.search.action.EvaluatedAction import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.slf4j.LoggerFactory @@ -138,7 +139,7 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { } } else { val leafGene = primaryGene.getLeafGene() - if (leafGene is ObjectGene || leafGene is ArrayGene<*>) { + if (leafGene is ObjectGene || leafGene is ArrayGene<*> || leafGene is FixedMapGene<*,*>) { return generateDtoCall(leafGene, actionName, lines).varName } } diff --git a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt index 8089c91ee1..426d1a8737 100644 --- a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt @@ -4,6 +4,13 @@ import java.util.* object StringUtils { + /** + * Capitalizes a word, lowercasing the rest of the word. For example, stringProperty would be modified into + * Stringproperty. + * + * @param word to be capitalized + * @return the capitalized word + */ fun capitalization(word: String) : String{ if(word.isEmpty()){ return word @@ -13,6 +20,23 @@ object StringUtils { word.substring(1).lowercase() } + /** + * Capitalizes the first char of a word, without modifying the rest. For example, stringProperty would be + * modified into StringProperty. This is useful for writing setter methods in output, where the name matches + * the property being serialized. + * + * @param word to be capitalized + * @return the capitalized word + */ + fun capitalizeFirstChar(word: String) : String { + if(word.isEmpty()){ + return word + } + + return word.substring(0, 1).uppercase() + + word.substring(1) + } + /** * A function for extracting the Simple Class Name from a class string. Given an inner class in the * OuterClass$InnerClass format, it will return the InnerClass value diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt index 65c4776b08..790094619f 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt @@ -15,6 +15,7 @@ import org.evomaster.core.search.Solution import org.evomaster.core.search.action.Action import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.`is` import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows @@ -107,7 +108,7 @@ class DtoWriterTest { assertEquals(collectedDtos.size, 1) val oneOfDto = collectedDtos[collectedDtos.keys.first()] assertNotNull(oneOfDto) - val dtoFields = oneOfDto?.fields?:emptyList() + val dtoFields = oneOfDto?.fieldsMap?:emptyMap() assertEquals(dtoFields.size, 2) assertDtoFieldIn(dtoFields, "dog", STRING) assertDtoFieldIn(dtoFields, "cat", STRING) @@ -125,7 +126,7 @@ class DtoWriterTest { assertEquals(collectedDtos.size, 1) val anyOfDto = collectedDtos[collectedDtos.keys.first()] assertNotNull(anyOfDto) - val dtoFields = anyOfDto?.fields?:emptyList() + val dtoFields = anyOfDto?.fieldsMap?:emptyMap() assertEquals(dtoFields.size, 2) assertDtoFieldIn(dtoFields, "email", STRING) assertDtoFieldIn(dtoFields, "numbers", "List") @@ -138,7 +139,8 @@ class DtoWriterTest { return actionCluster } - private fun assertDtoFieldIn(dtoFields: List, targetName: String, targetType: String) { - assertThat(dtoFields, hasItem(DtoField(targetName, targetType))) + private fun assertDtoFieldIn(dtoFields: Map, targetName: String, targetType: String) { + assertThat(dtoFields[targetName], `is`(DtoField(targetName, targetType))) } + }