diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index 681684915c..306d746182 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -1369,12 +1369,11 @@ object RestActionBuilderV3 { } /** - * * handled constraints include * - allOf * - anyOf * - oneOf - * - not (OpenAPI not support this yet) + * - not */ private fun assembleObjectGeneWithConstraints( name: String, @@ -1394,37 +1393,61 @@ object RestActionBuilderV3 { https://spec.openapis.org/oas/latest.html#discriminator-object */ - if (!options.enableConstraintHandling) + if (!options.enableConstraintHandling){ return assembleObjectGene(name, options, schema, fields, additionalFieldTemplate, referenceTypeName, examples, messages) + } + + /* + THIS IS VERY TRICKY + in theory, the use of allOf/anyOf/oneOf/not provides a rich language where many different kinds of + constraints can be specified. + this is the case when "const" is used in schemas to specify particular values, e.g., oneOf could be then + used to specify only particular combinations of field values. + the use of "const" is also quite tricky with "not", as can use to say some specific value combinations are not valid. + furthermore, those keywords are NOT mutually exclusive. + + support all possible constraint would be a GIGANTIC piece of work... yet, these kinds of constraints do + not seem so common, apart for allOf on merging fields. + TODO we should have full support, but not high priority task + */ + + /* + A further complication is how to use, and interpret, "examples" values (if any). + Eg. what to nested referenced $ref objects that have on examples? + should those be merged/hanled like the fields? + TODO possibly something to handle (but again, not high priority) + */ val allOf = schema.allOf?.map { s-> - //createObjectGene(name, s, swagger, history, null, enableConstraintHandling) getGene(name, s, schemaHolder,currentSchema, history, null, options, messages = messages, examples = examples) } val anyOf = schema.anyOf?.map { s-> - //createObjectGene(name, s, swagger, history, null, enableConstraintHandling) getGene(name, s, schemaHolder,currentSchema, history, null, options, messages = messages, examples = examples) } if (!allOf.isNullOrEmpty() && !anyOf.isNullOrEmpty()){ - messages.add("Cannot handle allOf and oneOf at same time for a schema with name $name") - return assembleObjectGene(name, options, schema, fields, additionalFieldTemplate, referenceTypeName, examples, messages) + messages.add("Currently cannot handle allOf and oneOf at same time for a schema with name $name") } val oneOf = schema.oneOf?.map { s-> - //createObjectGene(name, s, swagger, history, null, enableConstraintHandling) getGene(name, s, schemaHolder,currentSchema, history, null, options = options, messages = messages) } if (!oneOf.isNullOrEmpty() && (!allOf.isNullOrEmpty() || !anyOf.isNullOrEmpty())){ - messages.add("cannot handle oneOf and allOf/oneOf at same time for a schema with name $name") - return assembleObjectGene(name, options, schema, fields, additionalFieldTemplate, referenceTypeName, examples, messages) + messages.add("Currently cannot handle oneOf and allOf/oneOf at same time for a schema with name $name") } if (!allOf.isNullOrEmpty()){ + + if(allOf.size == 1 && fields.isEmpty()){ + //just use it as it is + return allOf[0] + } + val allFields = allOf.mapNotNull { when (it) { + is ChoiceGene<*> -> extractOriginalObject(it)?.fields is ObjectGene -> it.fields else -> null } @@ -1495,6 +1518,19 @@ object RestActionBuilderV3 { //TODO not } + private fun extractOriginalObject(choice: ChoiceGene<*>): ObjectGene? { + /* + this only makes sense if we are in this case: + Choice( Choice(examples), Object) + in which we would return Object, null otherwise + */ + val children = choice.getViewOfChildren() + if(children.size != 2 || children.none { it is UserExamplesGene && it.isUsedForExamples() }){ + return null + } + return children.find { it is ObjectGene } as ObjectGene? + } + /** * assemble ObjectGene based on [fields] and [additionalFieldTemplate] */ diff --git a/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt b/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt index a87e96af17..a5358e0ee1 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt @@ -2289,4 +2289,62 @@ class RestActionBuilderV3Test{ assertTrue(withFooAgain.contains("xfoo"), withFooAgain) assertTrue(withFooAgain.contains("ybar"), withFooAgain) } + + + @Test + fun testSingleAllOf() { + + val path = "/swagger/artificial/defaultandexamples/constraints/single_allof.yaml" + + val a = loadAndAssertActions(path, 1, RestActionBuilderV3.Options(probUseExamples = 1.0)) + .values.first() + + val rand = Randomness() + a.doInitialize(rand) + + val found = mutableSetOf() + + for (i in 0..100) { + a.randomize(rand, false) + val s = a.seeTopGenes().first().getValueAsRawString() + found.add(s) + if(found.size == 2){ + break + } + } + + //both examples must be there + assertEquals(2, found.size) + assertTrue(found.any{it.contains("Foo")}) + assertTrue(found.any{it.contains("Bar")}) + } + + + @Test + fun testMultiAllOf() { + + val path = "/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml" + + val a = loadAndAssertActions(path, 1, RestActionBuilderV3.Options(probUseExamples = 1.0)) + .values.first() + + val rand = Randomness() + a.doInitialize(rand) + + val found = mutableSetOf() + + for (i in 0..100) { + a.randomize(rand, false) + val s = a.seeTopGenes().first().getValueAsRawString() + found.add(s) + } + + /* + Only 1 example should be there, the one declared at top level. + TODO: maybe in future we merge examples as well... in that case, then + would need to update this test + */ + assertTrue(found.any{it.contains("Foo")}) + assertEquals(1, found.size) + } } diff --git a/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml new file mode 100644 index 0000000000..20e076c79d --- /dev/null +++ b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml @@ -0,0 +1,46 @@ +--- +openapi: 3.1.0 +info: + title: a schema + version: 1.0.0 +servers: + - url: "/v2" +paths: + "/api": + post: + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + additionalProperties: false + properties: + id: + type: integer + name: + type: string + examples: + - id : 42 + name: Bar + - $ref: '#/components/schemas/Dto' + examples: + Foo: + value: + id: 123 + name: Foo + extra: some extra + responses: + '200': + description: OK +components: + schemas: + Dto: + type: object + additionalProperties: false + properties: + extra: + type: string + example: + extra: Hello \ No newline at end of file diff --git a/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml new file mode 100644 index 0000000000..b229f428f2 --- /dev/null +++ b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml @@ -0,0 +1,34 @@ +--- +openapi: 3.1.0 +info: + title: a schema + version: 1.0.0 +servers: + - url: "/v2" +paths: + "/api": + post: + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + additionalProperties: false + properties: + id: + type: integer + name: + type: string + examples: + - id : 42 + name: Bar + examples: + Foo: + value: + id: 123 + name: Foo + responses: + '200': + description: OK \ No newline at end of file