From 71a017703d0d4e92b368f51c42753e00037719d1 Mon Sep 17 00:00:00 2001 From: Maks Oleksyuk Date: Wed, 1 Oct 2025 23:26:30 +0300 Subject: [PATCH] feature: add custom content-type for response --- .../OpenApiSpecGenerators/BaseGenerator.php | 13 +- tests/Unit/OpenAPISpecWriterTest.php | 221 ++++++++++++++++++ 2 files changed, 229 insertions(+), 5 deletions(-) diff --git a/src/Writing/OpenApiSpecGenerators/BaseGenerator.php b/src/Writing/OpenApiSpecGenerators/BaseGenerator.php index da3336f7..3052cba9 100644 --- a/src/Writing/OpenApiSpecGenerators/BaseGenerator.php +++ b/src/Writing/OpenApiSpecGenerators/BaseGenerator.php @@ -328,13 +328,16 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ]; } + $response = $endpoint->responses->where('content', $responseContent)->first(); + $contentType = $response->headers['content-type'] ?? $response->headers['Content-Type'] ?? 'application/json'; + switch ($type = gettype($decoded)) { case 'string': case 'boolean': case 'integer': case 'double': return [ - 'application/json' => [ + $contentType => [ 'schema' => [ 'type' => $type === 'double' ? 'number' : $type, 'example' => $decoded, @@ -346,7 +349,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE if (!count($decoded)) { // empty array return [ - 'application/json' => [ + $contentType => [ 'schema' => [ 'type' => 'array', 'items' => [ @@ -360,7 +363,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE // Non-empty array return [ - 'application/json' => [ + $contentType => [ 'schema' => [ 'type' => 'array', 'items' => [ @@ -378,7 +381,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE $required = $this->filterRequiredResponseFields($endpoint, array_keys($properties)); $data = [ - 'application/json' => [ + $contentType => [ 'schema' => [ 'type' => 'object', 'example' => $decoded, @@ -387,7 +390,7 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE ], ]; if ($required) { - $data['application/json']['schema']['required'] = $required; + $data[$contentType]['schema']['required'] = $required; } return $data; diff --git a/tests/Unit/OpenAPISpecWriterTest.php b/tests/Unit/OpenAPISpecWriterTest.php index b3bedfa1..152d30c8 100644 --- a/tests/Unit/OpenAPISpecWriterTest.php +++ b/tests/Unit/OpenAPISpecWriterTest.php @@ -616,6 +616,227 @@ public function adds_responses_correctly_as_responses_on_operation_object() ], $results['paths']['/path2']['put']['responses']); } + /** @test */ + public function adds_response_content_type_correctly() + { + $endpointData1 = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/path1', + 'responses' => [ + [ + 'status' => 404, + 'description' => 'No Found', + 'content' => '{"this": "shouldn\'t be ignored"}', + 'headers' => [ + 'Content-Type' => 'application/problem+json', + ] + ], + ] + ]); + $endpointData2 = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/path2', + 'responses' => [ + [ + 'status' => 404, + 'description' => 'No Found', + 'content' => '{"this": "shouldn\'t be ignored"}', + 'headers' => [ + 'content-type' => 'application/problem+json', + ] + ], + ] + ]); + $endpointData3 = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/path3', + 'responses' => [ + [ + 'status' => 404, + 'description' => 'No Found', + 'content' => '{"this": "shouldn\'t be ignored"}', + ], + ] + ]); + + $groups = [$this->createGroup([$endpointData1, $endpointData2, $endpointData3])]; + $results = $this->generate($groups); + + $this->assertCount(1, $results['paths']['/path1']['get']['responses']); + $this->assertArraySubset([ + '404' => [ + 'description' => 'No Found', + 'content' => [ + 'application/problem+json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'this' => [ + 'example' => "shouldn't be ignored", + 'type' => 'string', + ], + ], + ], + ], + ], + ], + ], $results['paths']['/path1']['get']['responses']); + + $this->assertCount(1, $results['paths']['/path2']['get']['responses']); + $this->assertArraySubset([ + '404' => [ + 'description' => 'No Found', + 'content' => [ + 'application/problem+json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'this' => [ + 'example' => "shouldn't be ignored", + 'type' => 'string', + ], + ], + ], + ], + ], + ], + ], $results['paths']['/path2']['get']['responses']); + + $this->assertCount(1, $results['paths']['/path3']['get']['responses']); + $this->assertArraySubset([ + '404' => [ + 'description' => 'No Found', + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'this' => [ + 'example' => "shouldn't be ignored", + 'type' => 'string', + ], + ], + ], + ], + ], + ], + ], $results['paths']['/path3']['get']['responses']); + } + + /** @test */ + public function handles_custom_content_type_with_various_response_body_types() + { + $customJsonType = 'application/vnd.api+json'; + + $endpointWithArray = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/array-response', + 'responses' => [[ + 'status' => 200, + 'content' => '["foo", "bar"]', + 'headers' => ['Content-Type' => $customJsonType], + ]], + ]); + + $endpointWithString = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/string-response', + 'responses' => [[ + 'status' => 200, + 'content' => '"a simple string"', + 'headers' => ['Content-Type' => $customJsonType], + ]], + ]); + + $endpointWithInteger = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/integer-response', + 'responses' => [[ + 'status' => 200, + 'content' => '123', + 'headers' => ['Content-Type' => $customJsonType], + ]], + ]); + + + $groups = [$this->createGroup([$endpointWithArray, $endpointWithString, $endpointWithInteger])]; + $results = $this->generate($groups); + + $arrayResponseSpec = $results['paths']['/array-response']['get']['responses']['200']; + $this->assertArrayHasKey($customJsonType, $arrayResponseSpec['content']); + $this->assertEquals('array', $arrayResponseSpec['content'][$customJsonType]['schema']['type']); + $this->assertEquals(['foo', 'bar'], $arrayResponseSpec['content'][$customJsonType]['schema']['example']); + + $stringResponseSpec = $results['paths']['/string-response']['get']['responses']['200']; + $this->assertArrayHasKey($customJsonType, $stringResponseSpec['content']); + $this->assertEquals('string', $stringResponseSpec['content'][$customJsonType]['schema']['type']); + $this->assertEquals('a simple string', $stringResponseSpec['content'][$customJsonType]['schema']['example']); + + $integerResponseSpec = $results['paths']['/integer-response']['get']['responses']['200']; + $this->assertArrayHasKey($customJsonType, $integerResponseSpec['content']); + $this->assertEquals('integer', $integerResponseSpec['content'][$customJsonType]['schema']['type']); + $this->assertEquals(123, $integerResponseSpec['content'][$customJsonType]['schema']['example']); + } + + /** @test */ + public function handles_non_json_response_content_as_text_plain() + { + $endpoint = $this->createMockEndpointData([ + 'httpMethods' => ['GET'], + 'uri' => '/text-response', + 'responses' => [[ + 'status' => 200, + 'content' => 'This is a simple text response.', + ]], + ]); + + $groups = [$this->createGroup([$endpoint])]; + $results = $this->generate($groups); + + $responseSpec = $results['paths']['/text-response']['get']['responses']['200']; + $this->assertArrayHasKey('text/plain', $responseSpec['content']); + $this->assertEquals('string', $responseSpec['content']['text/plain']['schema']['type']); + $this->assertEquals('This is a simple text response.', $responseSpec['content']['text/plain']['schema']['example']); + } + + /** @test */ + public function handles_null_and_empty_array_response_content() + { + $endpointWithNullContent = $this->createMockEndpointData([ + 'uri' => '/null-response', + 'httpMethods' => ['GET'], + 'responses' => [[ + 'status' => 200, + 'content' => null, + ]], + ]); + + $endpointWithEmptyArray = $this->createMockEndpointData([ + 'uri' => '/empty-array-response', + 'httpMethods' => ['GET'], + 'responses' => [[ + 'status' => 200, + 'content' => '[]', + ]], + ]); + + $groups = [$this->createGroup([$endpointWithNullContent, $endpointWithEmptyArray])]; + $results = $this->generate($groups); + + $nullResponseSpec = $results['paths']['/null-response']['get']['responses']['200']; + $this->assertArrayHasKey('application/json', $nullResponseSpec['content']); + $schemaForNull = $nullResponseSpec['content']['application/json']['schema']; + $this->assertEquals('object', $schemaForNull['type']); + $this->assertTrue($schemaForNull['nullable']); + + $emptyArrayResponseSpec = $results['paths']['/empty-array-response']['get']['responses']['200']; + $this->assertArrayHasKey('application/json', $emptyArrayResponseSpec['content']); + $schemaForEmptyArray = $emptyArrayResponseSpec['content']['application/json']['schema']; + $this->assertEquals('array', $schemaForEmptyArray['type']); + $this->assertEquals(['type' => 'object'], $schemaForEmptyArray['items']); // Scribe defaults items to 'object' for empty arrays + $this->assertEquals([], $schemaForEmptyArray['example']); + } + /** @test */ public function adds_required_fields_on_array_of_objects() {