-
Notifications
You must be signed in to change notification settings - Fork 114
json patch dto conversion #1603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
55abe37
1af1234
32ebb56
d79a3e3
2f1f033
5e05132
fe82830
0054b4e
91b0e56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,11 @@ 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 | ||
| import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene | ||
| import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene | ||
| import org.evomaster.core.search.gene.jsonpatch.JsonPatchOperationGene | ||
| import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathOnlyGene | ||
| import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene | ||
| import org.evomaster.core.search.gene.numeric.DoubleGene | ||
| import org.evomaster.core.search.gene.numeric.FloatGene | ||
| import org.evomaster.core.search.gene.numeric.IntegerGene | ||
|
|
@@ -36,6 +41,18 @@ class GeneToDto( | |
| val outputFormat: OutputFormat | ||
| ) { | ||
|
|
||
| companion object { | ||
| // Shared DTO class name for all JSON Patch operations (RFC 6902). | ||
| const val JSON_PATCH_OPERATION_DTO = "JsonPatchOperation" | ||
| const val FIELD_OP = "op" | ||
| const val FIELD_PATH = "path" | ||
| const val FIELD_FROM = "from" | ||
| const val FIELD_VALUE = "value" | ||
| const val TYPE_STRING = "String" | ||
| const val TYPE_JAVA_OBJECT = "Object" | ||
| const val TYPE_KOTLIN_ANY = "Any" | ||
| } | ||
|
|
||
| private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java) | ||
|
|
||
| private var dtoOutput: DtoOutput = if (outputFormat.isJava()) { | ||
|
|
@@ -67,6 +84,7 @@ class GeneToDto( | |
| } | ||
| is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback) | ||
| is FixedMapGene<*,*> -> TestWriterUtils.safeVariableName(fallback) | ||
| is JsonPatchDocumentGene -> JSON_PATCH_OPERATION_DTO | ||
| else -> throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $fallback") | ||
| } | ||
| } | ||
|
|
@@ -85,10 +103,90 @@ class GeneToDto( | |
| is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counters, null, capitalize) | ||
| is ChoiceGene<*> -> getDtoCall(gene.activeGene(), dtoName, counters, capitalize) | ||
| is FixedMapGene<*,*> -> getFixedMapGeneDtoCall(gene, dtoName, counters) | ||
| is JsonPatchDocumentGene -> getJsonPatchDtoCall(gene, counters) | ||
| else -> throw RuntimeException("BUG: Gene $gene (with type ${this::class.java.simpleName}) should not be creating DTOs") | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Renders a JSON Patch document as a List<JsonPatchOperation>, one DTO per active operation. | ||
| */ | ||
| private fun getJsonPatchDtoCall(gene: JsonPatchDocumentGene, counters: MutableList<Int>): DtoCall { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replace comment with Javadoc |
||
| val listVarName = "list_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" | ||
| val result = mutableListOf<String>() | ||
| result.add(dtoOutput.getNewListStatement(JSON_PATCH_OPERATION_DTO, listVarName)) | ||
|
|
||
| var operationCounter = 1 | ||
| gene.operations.forEach { operation -> | ||
| val childCounter = mutableListOf<Int>().apply { | ||
| addAll(counters) | ||
| add(operationCounter++) | ||
| } | ||
| val operationCall = getJsonPatchOperationCall(operation, childCounter) | ||
| result.addAll(operationCall.objectCalls) | ||
| result.add(dtoOutput.getAddElementToListStatement(listVarName, operationCall.varName)) | ||
| } | ||
|
|
||
| return DtoCall(listVarName, result) | ||
| } | ||
|
|
||
| /** | ||
| * Renders a single RFC 6902 operation as a JsonPatchOperation DTO with only its relevant fields set. | ||
| */ | ||
| private fun getJsonPatchOperationCall(operation: JsonPatchOperationGene, counters: MutableList<Int>): DtoCall { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replace comment with Javadoc |
||
| val varName = "dto_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" | ||
| val result = mutableListOf<String>() | ||
| result.add(dtoOutput.getNewObjectStatement(JSON_PATCH_OPERATION_DTO, varName)) | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_OP, "\"${operation.operationName}\"")) | ||
|
|
||
| when (operation) { | ||
| is JsonPatchPathOnlyGene -> { | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(operation.pathGene))) | ||
| } | ||
| is JsonPatchFromPathGene -> { | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_FROM, renderLeafValue(operation.fromGene))) | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(operation.pathGene))) | ||
| } | ||
| is JsonPatchPathValueGene -> { | ||
| val pair = operation.pathValueChoice.activeGene() | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(pair.first))) | ||
| setJsonPatchValue(varName, pair.second.getLeafGene(), counters, result) | ||
| } | ||
| } | ||
|
|
||
| return DtoCall(varName, result) | ||
| } | ||
|
|
||
| /** | ||
| * Sets the "value" field: primitives are inlined as literals, objects/arrays delegate to DTO generation. | ||
| */ | ||
| private fun setJsonPatchValue( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replace comment with Javadoc |
||
| varName: String, | ||
| valueGene: Gene, | ||
| counters: MutableList<Int>, | ||
| result: MutableList<String> | ||
| ) { | ||
| when (valueGene) { | ||
| is ObjectGene -> { | ||
| val childCall = getDtoCall(valueGene, getDtoName(valueGene, FIELD_VALUE, true), counters, true) | ||
| result.addAll(childCall.objectCalls) | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, childCall.varName)) | ||
| } | ||
| is ArrayGene<*> -> { | ||
| val childCall = getArrayDtoCall(valueGene, getDtoName(valueGene, FIELD_VALUE, true), counters, FIELD_VALUE, true) | ||
| result.addAll(childCall.objectCalls) | ||
| result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, childCall.varName)) | ||
| } | ||
| else -> result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, renderLeafValue(valueGene))) | ||
| } | ||
| } | ||
|
|
||
| // Returns the printable string representation of a gene's leaf value with its language-specific suffix. | ||
| private fun renderLeafValue(gene: Gene): String { | ||
| val leafGene = gene.getLeafGene() | ||
| return "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(leafGene)}" | ||
| } | ||
|
|
||
| private fun getObjectDtoCall(gene: ObjectGene, dtoName: String, counters: MutableList<Int>): DtoCall { | ||
| val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package org.evomaster.core.output.dto | ||
|
|
||
| import org.evomaster.core.TestUtils | ||
| import org.evomaster.core.output.OutputFormat | ||
| import org.evomaster.core.output.Termination | ||
| import org.evomaster.core.output.naming.RestActionTestCaseUtils.getEvaluatedIndividualWith | ||
| import org.evomaster.core.output.naming.RestActionTestCaseUtils.getRestCallAction | ||
| import org.evomaster.core.problem.api.param.Param | ||
| import org.evomaster.core.problem.enterprise.SampleType | ||
| import org.evomaster.core.problem.rest.data.HttpVerb | ||
| import org.evomaster.core.problem.rest.data.RestCallResult | ||
| import org.evomaster.core.problem.rest.data.RestIndividual | ||
| import org.evomaster.core.problem.rest.param.BodyParam | ||
| import org.evomaster.core.search.EvaluatedIndividual | ||
| import org.evomaster.core.search.FitnessValue | ||
| import org.evomaster.core.search.Solution | ||
| 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.jsonpatch.JsonPatchDocumentGene | ||
| import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene | ||
| import org.evomaster.core.search.gene.numeric.IntegerGene | ||
| import org.evomaster.core.search.gene.string.StringGene | ||
| import org.evomaster.core.search.service.Randomness | ||
| import org.junit.jupiter.api.Assertions.assertEquals | ||
| import org.junit.jupiter.api.Assertions.assertNotNull | ||
| import org.junit.jupiter.api.Assertions.assertTrue | ||
| import org.junit.jupiter.api.Test | ||
| import java.nio.file.Paths | ||
| import java.util.Collections.singletonList | ||
|
|
||
| class DtoWriterJsonPatchTest { | ||
|
|
||
| private val outputTestSuitePath = Paths.get("./target/dto-writer-json-patch-test") | ||
| private val testPackage = "test.package" | ||
|
|
||
| private fun jsonPatchBodyParam(): Param { | ||
| val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) | ||
| val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } | ||
| return BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) | ||
| } | ||
|
|
||
| private fun jsonPatchSolution(): Solution<*> { | ||
| val action = getRestCallAction("/items/{id}", HttpVerb.PATCH, mutableListOf(jsonPatchBodyParam())) | ||
| val eIndividual = getEvaluatedIndividualWith(action) | ||
| return Solution(singletonList(eIndividual), "", "", Termination.NONE, emptyList(), emptyList()) | ||
| } | ||
|
|
||
| @Test | ||
| fun collectsJsonPatchOperationDtoWithAllFields() { | ||
| val dtoWriter = DtoWriter(OutputFormat.KOTLIN_JUNIT_5) | ||
| dtoWriter.write(outputTestSuitePath, testPackage, jsonPatchSolution()) | ||
|
|
||
| val dtos = dtoWriter.getCollectedDtos() | ||
| val operationDto = dtos[GeneToDto.JSON_PATCH_OPERATION_DTO] | ||
| assertNotNull(operationDto, "Expected a ${GeneToDto.JSON_PATCH_OPERATION_DTO} DTO to be collected") | ||
|
|
||
| val fields = operationDto!!.fieldsMap | ||
| assertEquals(DtoField(GeneToDto.FIELD_OP, "String"), fields[GeneToDto.FIELD_OP]) | ||
| assertEquals(DtoField(GeneToDto.FIELD_PATH, "String"), fields[GeneToDto.FIELD_PATH]) | ||
| assertEquals(DtoField(GeneToDto.FIELD_FROM, "String"), fields[GeneToDto.FIELD_FROM]) | ||
| // value is the generic object type since a JSON Patch value can be any JSON value | ||
| assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Any"), fields[GeneToDto.FIELD_VALUE]) | ||
| } | ||
|
|
||
| @Test | ||
| fun valueFieldIsObjectForJavaOutput() { | ||
| val dtoWriter = DtoWriter(OutputFormat.JAVA_JUNIT_5) | ||
| dtoWriter.write(outputTestSuitePath, testPackage, jsonPatchSolution()) | ||
|
|
||
| val operationDto = dtoWriter.getCollectedDtos()[GeneToDto.JSON_PATCH_OPERATION_DTO] | ||
| assertNotNull(operationDto) | ||
| assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Object"), operationDto!!.fieldsMap[GeneToDto.FIELD_VALUE]) | ||
| } | ||
|
|
||
| @Test | ||
| fun collectsNestedObjectDtoWhenValueIsArrayOfObjects() { | ||
| // Schema where the only patchable field is an array of objects: | ||
| // every add/replace/test operation will have an ArrayGene<ObjectGene> as its value gene, | ||
| // so calculateDtoFromJsonPatch must visit calculateDtoFromArray for those operations. | ||
| val tagSchema = ObjectGene("Tag", listOf(StringGene("label")), refType = "Tag") | ||
| val schema = ObjectGene("body", listOf(ArrayGene("tags", tagSchema))) | ||
| val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } | ||
| val bodyParam = BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) | ||
|
|
||
| val action = getRestCallAction("/items/{id}", HttpVerb.PATCH, mutableListOf(bodyParam)) | ||
| val individual = RestIndividual(mutableListOf(action), SampleType.RANDOM) | ||
| TestUtils.doInitializeIndividualForTesting(individual, Randomness().apply { updateSeed(42L) }) | ||
|
|
||
| val result = RestCallResult(action.getLocalId()).apply { setStatusCode(200) } | ||
| val ei = EvaluatedIndividual<RestIndividual>(FitnessValue(0.0), individual, listOf(result)) | ||
| val solution = Solution(singletonList(ei), "", "", Termination.NONE, emptyList(), emptyList()) | ||
|
|
||
| val dtoWriter = DtoWriter(OutputFormat.KOTLIN_JUNIT_5) | ||
| dtoWriter.write(outputTestSuitePath, testPackage, solution) | ||
| val dtos = dtoWriter.getCollectedDtos() | ||
|
|
||
| assertNotNull(dtos[GeneToDto.JSON_PATCH_OPERATION_DTO]) | ||
|
|
||
| val patchGene = bodyParam.primaryGene() as JsonPatchDocumentGene | ||
| val pathValueOps = patchGene.operations.filterIsInstance<JsonPatchPathValueGene>() | ||
| assertTrue(pathValueOps.isNotEmpty(), "Seed 42L must produce at least one add/replace/test operation") | ||
| // All add/replace/test operations carry an ArrayGene<ObjectGene> value in this schema; | ||
| // the nested Tag DTO must therefore be collected. | ||
| assertNotNull(dtos["Tag"], "Nested Tag DTO must be collected for operations with array-of-objects value") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replace comment with Javadoc