diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatch/BBJsonPatchApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatch/BBJsonPatchApplication.kt new file mode 100644 index 0000000000..99ab42e66a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatch/BBJsonPatchApplication.kt @@ -0,0 +1,104 @@ +package com.foo.rest.examples.bb.jsonpatch + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.evomaster.e2etests.utils.CoveredTargets +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/pets") +open class BBJsonPatchApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BBJsonPatchApplication::class.java, *args) + } + } + + private val store: MutableMap = mutableMapOf( + 1L to BBJsonPatchDto("Dog", 3), + 2L to BBJsonPatchDto("Cat", 5) + ) + + private val mapper = ObjectMapper() + + @GetMapping("/{id}") + fun getPet(@PathVariable id: Long): ResponseEntity { + val pet = store[id] ?: return ResponseEntity.badRequest().build() + return ResponseEntity.ok(pet) + } + + @PatchMapping("/{id}", consumes = ["application/json-patch+json"]) + fun patchPet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + if (parsePatchDocument(body) == null) + return ResponseEntity.badRequest().body("Patch document must be a JSON array") + + CoveredTargets.cover("PATCHED") + return ResponseEntity.ok("patched") + } + + @PatchMapping("/{id}/add", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun addPet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity = + patchOperation(body, "add", "JSON_PATCH_ADD") + + @PatchMapping("/{id}/remove", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun removePet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity = + patchOperation(body, "remove", "JSON_PATCH_REMOVE") + + @PatchMapping("/{id}/replace", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun replacePet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity = + patchOperation(body, "replace", "JSON_PATCH_REPLACE") + + @PatchMapping("/{id}/move", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun movePet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity = + patchOperation(body, "move", "JSON_PATCH_MOVE") + + @PatchMapping("/{id}/copy", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun copyPet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity = + patchOperation(body, "copy", "JSON_PATCH_COPY") + + @PatchMapping("/{id}/test", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun testPet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity = + patchOperation(body, "test", "JSON_PATCH_TEST") + + @PatchMapping("/{id}/sequence", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun sequencePet(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + if (!hasMultipleOperations(body)) + return ResponseEntity.badRequest().body("Patch document must contain at least two operations") + + CoveredTargets.cover("JSON_PATCH_SEQUENCE") + return ResponseEntity.ok("sequence patched") + } + + private fun patchOperation(body: String, operation: String, target: String): ResponseEntity { + if (!hasOperation(body, operation)) + return ResponseEntity.badRequest().body("Patch document must contain a $operation operation") + + CoveredTargets.cover(target) + return ResponseEntity.ok("$operation patched") + } + + private fun hasOperation(body: String, operation: String): Boolean = + parsePatchDocument(body)?.any { it.path("op").asText() == operation } ?: false + + private fun hasMultipleOperations(body: String): Boolean = + parsePatchDocument(body) + ?.takeIf { it.size() >= 2 } + ?.all { it.hasNonNull("op") } + ?: false + + private fun parsePatchDocument(body: String): JsonNode? = + try { + mapper.readTree(body).takeIf { it.isArray } + } catch (e: Exception) { + null + } +} + +data class BBJsonPatchDto(val name: String = "", val age: Int = 0) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/jsonpatch/BBJsonPatchController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/jsonpatch/BBJsonPatchController.kt new file mode 100644 index 0000000000..2a41003871 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/jsonpatch/BBJsonPatchController.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.bb.jsonpatch + +import com.foo.rest.examples.bb.SpringController + +class BBJsonPatchController : SpringController(BBJsonPatchApplication::class.java) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt new file mode 100644 index 0000000000..f4d5879149 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt @@ -0,0 +1,72 @@ +package org.evomaster.e2etests.spring.rest.bb.jsonpatch + +import com.foo.rest.examples.bb.jsonpatch.BBJsonPatchController +import org.evomaster.core.EMConfig +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.rest.bb.SpringTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +class BBJsonPatchTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + val config = EMConfig() + initClass(BBJsonPatchController(), config) + } + } + + @ParameterizedTest + @EnumSource + fun testBlackBoxOutput(outputFormat: OutputFormat) { + executeAndEvaluateBBTest( + outputFormat, + "BBJsonPatchEM", + 1000, + 3, + listOf( + "PATCHED", + "JSON_PATCH_ADD", + "JSON_PATCH_REMOVE", + "JSON_PATCH_REPLACE", + "JSON_PATCH_MOVE", + "JSON_PATCH_COPY", + "JSON_PATCH_TEST", + "JSON_PATCH_SEQUENCE" + ) + ) { args: MutableList -> + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}", "patched") + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/add", "add patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/add", null) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/remove", "remove patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/remove", null) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/replace", "replace patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/replace", null) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/move", "move patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/move", null) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/copy", "copy patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/copy", null) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/test", "test patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/test", null) + + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/pets/{id}/sequence", "sequence patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/sequence", null) + } + } +} \ No newline at end of file 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 64d229d788..7a0351a2eb 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 @@ -22,6 +22,7 @@ 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.jsonpatch.JsonPatchDocumentGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene @@ -118,6 +119,11 @@ class DtoWriter( gene is ObjectGene -> calculateDtoFromObject(gene, actionName) gene is ArrayGene<*> -> calculateDtoFromArray(gene, actionName) gene is FixedMapGene<*, *> -> calculateDtoFromFixedMapGene(gene, actionName) + // TODO: a JsonPatchDocumentGene is currently skipped from DTO collection. Once we decide + // how a JSON Patch document should be rendered when a test case is written (it is not a + // regular object/array DTO but an RFC 6902 array of operations), this should build and + // emit the corresponding DTO instead of returning. + gene is JsonPatchDocumentGene -> return isPrimitiveGene(gene) -> return else -> { throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $actionName") 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 56221bd711..44e268d9b4 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 @@ -29,6 +29,7 @@ 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.jsonpatch.JsonPatchDocumentGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.slf4j.LoggerFactory @@ -127,6 +128,9 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { val bodyParam = call.parameters.find { p -> p is BodyParam } as BodyParam? if (bodyParam != null && bodyParam.isJson() && payloadIsValidJson(bodyParam)) { val primaryGene = bodyParam.primaryGene() + if (primaryGene.getWrappedGene(JsonPatchDocumentGene::class.java) != null) { + return "" + } val choiceGene = primaryGene.getWrappedGene(ChoiceGene::class.java) val actionName = call.getName() if (choiceGene != null) { diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/JsonPatchSchemaResolver.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/JsonPatchSchemaResolver.kt new file mode 100644 index 0000000000..af2fc95573 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/JsonPatchSchemaResolver.kt @@ -0,0 +1,106 @@ +package org.evomaster.core.problem.rest.builder + +import io.swagger.v3.oas.models.Operation +import io.swagger.v3.oas.models.PathItem +import io.swagger.v3.oas.models.media.MediaType +import io.swagger.v3.oas.models.media.Schema +import org.evomaster.core.problem.rest.schema.RestSchema +import org.evomaster.core.problem.rest.schema.SchemaOpenAPI +import org.evomaster.core.problem.rest.schema.SchemaUtils + +/** + * Resolves the target resource schema for a JSON Patch endpoint by inspecting + * sibling operations on the same path item. + * + * Priority: GET 2xx response → PUT requestBody → POST requestBody. + */ +object JsonPatchSchemaResolver { + + private const val JSON_PATCH_MEDIA_TYPE = "json-patch" + + fun resolveResourceSchema( + operation: Operation, + schemaHolder: RestSchema, + currentSchema: SchemaOpenAPI, + messages: MutableList + ): Schema<*>? { + val pathItem = findPathItemForPatchOperation(operation, schemaHolder, currentSchema, messages) + ?: return null + + return fromGetResponse(pathItem, schemaHolder, currentSchema, messages) + ?: fromRequestBody(pathItem.put, schemaHolder, currentSchema, messages) + ?: fromRequestBody(pathItem.post, schemaHolder, currentSchema, messages) + } + + //handleBodyPayload does not have the path of the operation, we need to find it, only if it is patch + private fun findPathItemForPatchOperation( + operation: Operation, + schemaHolder: RestSchema, + currentSchema: SchemaOpenAPI, + messages: MutableList + ): PathItem? { + return schemaHolder.main.schemaParsed.paths + ?.values + ?.firstNotNullOfOrNull { pathItemOrRef -> + val pathItem = if (pathItemOrRef.`$ref` != null) { + SchemaUtils.getReferencePathItem(schemaHolder, currentSchema, pathItemOrRef.`$ref`, messages) + } else { + pathItemOrRef + } + + pathItem?.takeIf { it.patch === operation } + } + } + + // For get, the resource is in the response + private fun fromGetResponse( + pathItem: PathItem, + schemaHolder: RestSchema, + currentSchema: SchemaOpenAPI, + messages: MutableList + ): Schema<*>? { + val get = pathItem.get ?: return null + return get.responses + ?.filter { (code, _) -> code.startsWith("2") } + ?.values + ?.firstNotNullOfOrNull { response -> + val resolved = if (response.`$ref` != null) { + SchemaUtils.getReferenceResponse(schemaHolder, currentSchema, response.`$ref`, messages) + ?: return@firstNotNullOfOrNull null + } else response + extractJsonSchema(resolved.content, schemaHolder, currentSchema, messages) + } + } + + // For put and post, the resource is in the requestBody + private fun fromRequestBody( + operation: Operation?, + schemaHolder: RestSchema, + currentSchema: SchemaOpenAPI, + messages: MutableList + ): Schema<*>? { + val body = operation?.requestBody ?: return null + val resolvedBody = if (body.`$ref` != null) { + SchemaUtils.getReferenceRequestBody(schemaHolder, currentSchema, body.`$ref`, messages) + ?: return null + } else body + return extractJsonSchema(resolvedBody.content, schemaHolder, currentSchema, messages) + } + + private fun extractJsonSchema( + content: Map?, + schemaHolder: RestSchema, + currentSchema: SchemaOpenAPI, + messages: MutableList + ): Schema<*>? { + val schema = content + ?.filterKeys { mt -> mt.contains("json") && !mt.contains(JSON_PATCH_MEDIA_TYPE) } + ?.values + ?.firstOrNull() + ?.schema + ?: return null + return if (schema.`$ref` != null) { + SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, schema.`$ref`, messages) + } else schema + } +} 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 8f322cc1af..6f9fd44dbc 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 @@ -47,6 +47,7 @@ import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.evomaster.core.search.gene.wrapper.CustomMutationRateGene import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.search.gene.placeholder.CycleObjectGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene import org.evomaster.core.search.gene.placeholder.LimitObjectGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene @@ -747,11 +748,37 @@ object RestActionBuilderV3 { listOf() } - // $ref schemas do not carry XML metadata; resolving the reference is required to obtain the correct XML element name from the target schema - val deref = obj.schema.`$ref`?.let { ref -> SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema - val name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" + val isJsonPatch = verb == HttpVerb.PATCH && bodies.keys.any { it.contains("json-patch") } - var gene = getGene(name, obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) + val name: String + var gene: Gene + if (isJsonPatch) { + /* + The body is a JSON Patch document (RFC 6902), not a regular object, so it is not built + from the media type schema. resolveResourceSchema returns the OpenAPI Schema of the resource + being patched, found by inspecting sibling operations on the same path (GET 2xx response, + else PUT/POST requestBody). We turn that schema into a gene via getGene so the patch + operations reference real fields/paths of the resource, and use it to seed the + JsonPatchDocumentGene. If no resource schema is found, the gene is still built with + resourceGene == null and emits generic, structurally valid operations. + */ + name = "body" + val patchResourceSchema = JsonPatchSchemaResolver.resolveResourceSchema( + operation, + schemaHolder, + currentSchema, + messages + ) + val resourceGene = patchResourceSchema?.let { + getGene(name, it, schemaHolder, currentSchema, referenceClassDef = null, options = options, messages = messages) + } + gene = JsonPatchDocumentGene(name, resourceGene) + } else { + // $ref schemas do not carry XML metadata; resolving the reference is required to obtain the correct XML element name from the target schema + val deref = obj.schema.`$ref`?.let { ref -> SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema + name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" + gene = getGene(name, obj.schema, schemaHolder, currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) + } if (resolvedBody.required != true && gene !is OptionalGene) { gene = OptionalGene(name, gene) diff --git a/core/src/test/kotlin/org/evomaster/core/problem/rest/builder/JsonPatchSchemaResolverTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/rest/builder/JsonPatchSchemaResolverTest.kt new file mode 100644 index 0000000000..d2c6b2d9ff --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/problem/rest/builder/JsonPatchSchemaResolverTest.kt @@ -0,0 +1,95 @@ +package org.evomaster.core.problem.rest.builder + +import org.evomaster.core.problem.rest.schema.OpenApiAccess +import org.evomaster.core.problem.rest.schema.RestSchema +import org.evomaster.core.problem.rest.schema.SchemaLocation +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class JsonPatchSchemaResolverTest { + + companion object { + private val schema: RestSchema by lazy { + val json = JsonPatchSchemaResolverTest::class.java + .getResourceAsStream("/swagger/artificial/jsonpatch/json-patch-schema-resolver.json")!! + .bufferedReader().readText() + RestSchema(OpenApiAccess.parseOpenApi(json, SchemaLocation.MEMORY)) + } + } + + private fun resolveForPatch( + path: String, + messages: MutableList = mutableListOf() + ) = JsonPatchSchemaResolver.resolveResourceSchema( + schema.main.schemaParsed.paths[path]!!.patch, + schema, + schema.main, + messages + ) + + @Test + fun testResolveFromGetResponse() { + val messages = mutableListOf() + + val result = resolveForPatch("/pets/{id}", messages) + + assertNotNull(result) + assertTrue(messages.isEmpty(), "Unexpected messages: $messages") + val props = result!!.properties + assertNotNull(props) + assertTrue(props.containsKey("name"), "Expected property 'name'") + assertTrue(props.containsKey("age"), "Expected property 'age'") + } + + @Test + fun testPreferGetOverPut() { + val result = resolveForPatch("/x/{id}") + + assertNotNull(result) + val props = result!!.properties + assertTrue(props.containsKey("fromGet"), "Should prefer GET schema, got $props") + assertFalse(props.containsKey("fromPut")) + } + + @Test + fun testFallbackToPut() { + val result = resolveForPatch("/orders/{id}") + + assertNotNull(result) + val props = result!!.properties + assertTrue(props.containsKey("product"), "Expected 'product' in $props") + assertTrue(props.containsKey("quantity"), "Expected 'quantity' in $props") + } + + @Test + fun testFallbackToPost() { + val result = resolveForPatch("/users") + + assertNotNull(result) + assertTrue(result!!.properties.containsKey("email")) + } + + @Test + fun testReturnsNullWhenNoSiblings() { + val result = resolveForPatch("/items/{id}") + + assertNull(result, "Expected null when no sibling operations define a JSON schema") + } + + @Test + fun testIgnoresJsonPatchContentTypeInGetResponse() { + val result = resolveForPatch("/docs/{id}") + + assertNull(result, "Should not use json-patch content type as resource schema") + } + + @Test + fun testResolveFromGetResponseViaRef() { + val result = resolveForPatch("/cats/{id}") + + assertNotNull(result) + val props = result!!.properties + assertTrue(props.containsKey("breed"), "Expected 'breed' via \$ref resolution, got $props") + assertTrue(props.containsKey("indoor"), "Expected 'indoor' via \$ref resolution, got $props") + } +} diff --git a/core/src/test/resources/swagger/artificial/jsonpatch/json-patch-schema-resolver.json b/core/src/test/resources/swagger/artificial/jsonpatch/json-patch-schema-resolver.json new file mode 100644 index 0000000000..5d2cfa5c94 --- /dev/null +++ b/core/src/test/resources/swagger/artificial/jsonpatch/json-patch-schema-resolver.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.0.0", + "info": {"title": "t", "version": "1"}, + "paths": { + "/pets/{id}": { + "get": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"} + } + } + } + } + } + } + }, + "patch": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "required": true, + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + }, + "/x/{id}": { + "get": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "responses": { + "200": { + "content": {"application/json": {"schema": {"type": "object", "properties": {"fromGet": {"type": "string"}}}}} + } + } + }, + "put": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json": {"schema": {"type": "object", "properties": {"fromPut": {"type": "string"}}}}} + }, + "responses": {"200": {"description": "ok"}} + }, + "patch": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + }, + "/orders/{id}": { + "put": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json": {"schema": {"type": "object", "properties": {"product": {"type": "string"}, "quantity": {"type": "integer"}}}}} + }, + "responses": {"200": {"description": "ok"}} + }, + "patch": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + }, + "/users": { + "post": { + "requestBody": { + "content": {"application/json": {"schema": {"type": "object", "properties": {"email": {"type": "string"}}}}} + }, + "responses": {"200": {"description": "ok"}} + }, + "patch": { + "requestBody": { + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + }, + "/items/{id}": { + "patch": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + }, + "/docs/{id}": { + "get": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "responses": { + "200": { + "content": { + "application/json-patch+json": { + "schema": {"type": "array", "items": {"type": "object"}} + } + } + } + } + }, + "patch": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + }, + "/cats/{id}": { + "get": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Cat"} + } + } + } + } + }, + "patch": { + "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], + "requestBody": { + "content": {"application/json-patch+json": {"schema": {"type": "array", "items": {"type": "object"}}}} + }, + "responses": {"200": {"description": "ok"}} + } + } + }, + "components": { + "schemas": { + "Cat": { + "type": "object", + "properties": { + "breed": {"type": "string"}, + "indoor": {"type": "boolean"} + } + } + } + } +}