From 10f4c435c9870292a6cf7f84d955eeeb6941e4b8 Mon Sep 17 00:00:00 2001 From: Rouven Hurling Date: Wed, 24 Jan 2024 12:05:57 +0100 Subject: [PATCH 1/3] rewrite generateEndpointResponsesSpec to allow multiple content types and schemas (via oneOf) according to OpenAPI 3.0 spec --- src/Writing/OpenAPISpecWriter.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index 509c01f0..f1f1f5ff 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -263,12 +263,34 @@ protected function generateEndpointResponsesSpec(OutputEndpointData $endpoint) foreach ($endpoint->responses as $response) { // OpenAPI groups responses by status code - // Only one response type per status code, so only the last one will be used + // OpenAPI 3.0 allows multiple different response content types and schemas per content type (via oneOf), so all responses will be used and merged (but not deduplicated) + // Only the first response per status code sets the overall description of the response status code if (intval($response->status) === 204) { // Must not add content for 204 $responses[204] = [ 'description' => $this->getResponseDescription($response), ]; + } elseif (isset($responses[$response->status])) { + $content = $this->generateResponseContentSpec($response->content, $endpoint); + $contentType = array_keys($content)[0]; + if (isset($responses[$response->status]['content'][$contentType])) { + if (!isset($responses[$response->status]['content'][$contentType]['schema']['oneOf'])) { + $oldSchema = array_replace([ + 'description' => $responses[$response->status]['description'], + ], $responses[$response->status]['content'][$contentType]['schema']); + + $responses[$response->status]['content'][$contentType]['schema'] = [ + 'oneOf' => [$newSchema], + ]; + } + $newSchema = array_replace([ + 'description' => $this->getResponseDescription($response), + ], $content[$contentType]['schema']); + + $responses[$response->status]['content'][$contentType]['schema']['oneOf'][] = $newSchema; + } else { + $responses[$response->status]['content'][$contentType] = $content[$contentType]; + } } else { $responses[$response->status] = [ 'description' => $this->getResponseDescription($response), From ec4b6dffd1390874599db1032897b41169bc220c Mon Sep 17 00:00:00 2001 From: Shalvah Date: Mon, 17 Jun 2024 22:23:33 +0200 Subject: [PATCH 2/3] Update OpenAPISpecWriter.php --- src/Writing/OpenAPISpecWriter.php | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php index f1f1f5ff..2c4f7973 100644 --- a/src/Writing/OpenAPISpecWriter.php +++ b/src/Writing/OpenAPISpecWriter.php @@ -263,35 +263,38 @@ protected function generateEndpointResponsesSpec(OutputEndpointData $endpoint) foreach ($endpoint->responses as $response) { // OpenAPI groups responses by status code - // OpenAPI 3.0 allows multiple different response content types and schemas per content type (via oneOf), so all responses will be used and merged (but not deduplicated) - // Only the first response per status code sets the overall description of the response status code + // Only one response type per status code, so only the last one will be used if (intval($response->status) === 204) { // Must not add content for 204 $responses[204] = [ 'description' => $this->getResponseDescription($response), ]; } elseif (isset($responses[$response->status])) { + // If we already have a response for this status code and content type, + // we change to a `oneOf` which includes all the responses $content = $this->generateResponseContentSpec($response->content, $endpoint); $contentType = array_keys($content)[0]; if (isset($responses[$response->status]['content'][$contentType])) { - if (!isset($responses[$response->status]['content'][$contentType]['schema']['oneOf'])) { - $oldSchema = array_replace([ + // If we've already created the oneOf object, add this response + if (isset($responses[$response->status]['content'][$contentType]['schema']['oneOf'])) { + $responses[$response->status]['content'][$contentType]['schema']['oneOf'][] = $content[$contentType]; + } else { + // Create the oneOf object + $existingResponseExample = array_replace([ 'description' => $responses[$response->status]['description'], ], $responses[$response->status]['content'][$contentType]['schema']); + $newResponseExample = array_replace([ + 'description' => $this->getResponseDescription($response), + ], $content[$contentType]['schema']); + $responses[$response->status]['description'] = ''; $responses[$response->status]['content'][$contentType]['schema'] = [ - 'oneOf' => [$newSchema], + 'oneOf' => [$existingResponseExample, $newResponseExample] ]; } - $newSchema = array_replace([ - 'description' => $this->getResponseDescription($response), - ], $content[$contentType]['schema']); - - $responses[$response->status]['content'][$contentType]['schema']['oneOf'][] = $newSchema; - } else { - $responses[$response->status]['content'][$contentType] = $content[$contentType]; } } else { + // Store as the response for this status $responses[$response->status] = [ 'description' => $this->getResponseDescription($response), 'content' => $this->generateResponseContentSpec($response->content, $endpoint), From 8a4a42a36d6ec55f3405592ccbb3a55321ba8a70 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Mon, 17 Jun 2024 22:25:22 +0200 Subject: [PATCH 3/3] Add test --- tests/Unit/OpenAPISpecWriterTest.php | 81 +++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/tests/Unit/OpenAPISpecWriterTest.php b/tests/Unit/OpenAPISpecWriterTest.php index 6d2c94a5..648e3529 100644 --- a/tests/Unit/OpenAPISpecWriterTest.php +++ b/tests/Unit/OpenAPISpecWriterTest.php @@ -445,7 +445,7 @@ public function adds_responses_correctly_as_responses_on_operation_object() 'type' => 'string', 'description' => 'Parameter description, ha!', ], - 'sub level 0.sub level 1 key 3.sub level 2 key 1'=> [ + 'sub level 0.sub level 1 key 3.sub level 2 key 1' => [ 'description' => 'This is description of nested object', ] ], @@ -557,6 +557,85 @@ public function adds_responses_correctly_as_responses_on_operation_object() ], $results['paths']['/path2']['put']['responses']); } + /** @test */ + public function adds_multiple_responses_correctly_using_oneOf() + { + $endpointData1 = $this->createMockEndpointData([ + 'httpMethods' => ['POST'], + 'uri' => '/path1', + 'responses' => [ + [ + 'status' => 201, + 'description' => 'This one', + 'content' => '{"this": "one"}', + ], + [ + 'status' => 201, + 'description' => 'No, that one.', + 'content' => '{"that": "one"}', + ], + [ + 'status' => 200, + 'description' => 'A separate one', + 'content' => '{"the other": "one"}', + ], + ], + ]); + $groups = [$this->createGroup([$endpointData1])]; + + $results = $this->generate($groups); + + $this->assertArraySubset([ + '200' => [ + 'description' => 'A separate one', + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'the other' => [ + 'example' => "one", + 'type' => 'string', + ], + ], + ], + ], + ], + ], + '201' => [ + 'description' => '', + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'oneOf' => [ + [ + 'type' => 'object', + 'description' => 'This one', + 'properties' => [ + 'this' => [ + 'example' => "one", + 'type' => 'string', + ], + ], + ], + [ + 'type' => 'object', + 'description' => 'No, that one.', + 'properties' => [ + 'that' => [ + 'example' => "one", + 'type' => 'string', + ], + ], + ], + ], + ], + ], + ], + ], + ], $results['paths']['/path1']['post']['responses']); + } + protected function createMockEndpointData(array $custom = []): OutputEndpointData { $faker = Factory::create();