From ea71416bb527579b4f49674161bc1de47a14ee12 Mon Sep 17 00:00:00 2001 From: sajixavier Date: Mon, 27 Feb 2023 23:37:19 +0530 Subject: [PATCH] fix(openapi): allow overriding of openapi responses (#5393) Co-authored-by: Saji Xavier --- src/OpenApi/Factory/OpenApiFactory.php | 50 ++++-- tests/OpenApi/Factory/OpenApiFactoryTest.php | 151 +++++++++++++++++++ 2 files changed, 187 insertions(+), 14 deletions(-) diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 66c2fd93fe9..3d768645091 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -279,38 +279,44 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } } + $existingResponses = $openapiOperation?->getResponses() ?: []; // Create responses switch ($method) { case HttpOperation::METHOD_GET: $successStatus = (string) $operation->getStatus() ?: 200; - $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas); - $openapiOperation = $openapiOperation->withResponse($successStatus, new Response(sprintf('%s %s', $resourceShortName, $operation instanceof CollectionOperationInterface ? 'collection' : 'resource'), $responseContent)); + $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s %s', $resourceShortName, $operation instanceof CollectionOperationInterface ? 'collection' : 'resource'), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas); break; case HttpOperation::METHOD_POST: - $responseLinks = $this->getLinks($resourceMetadataCollection, $operation); - $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas); $successStatus = (string) $operation->getStatus() ?: 201; - $openapiOperation = $openapiOperation->withResponse($successStatus, new Response(sprintf('%s resource created', $resourceShortName), $responseContent, null, $responseLinks)); - $openapiOperation = $openapiOperation->withResponse(400, new Response('Invalid input')); - $openapiOperation = $openapiOperation->withResponse(422, new Response('Unprocessable entity')); + + $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s resource created', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection); + + $openapiOperation = $this->buildOpenApiResponse($existingResponses, '400', 'Invalid input', $openapiOperation); + + $openapiOperation = $this->buildOpenApiResponse($existingResponses, '422', 'Unprocessable entity', $openapiOperation); break; case HttpOperation::METHOD_PATCH: case HttpOperation::METHOD_PUT: - $responseLinks = $this->getLinks($resourceMetadataCollection, $operation); $successStatus = (string) $operation->getStatus() ?: 200; - $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas); - $openapiOperation = $openapiOperation->withResponse($successStatus, new Response(sprintf('%s resource updated', $resourceShortName), $responseContent, null, $responseLinks)); - $openapiOperation = $openapiOperation->withResponse(400, new Response('Invalid input')); - $openapiOperation = $openapiOperation->withResponse(422, new Response('Unprocessable entity')); + $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s resource updated', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection); + $openapiOperation = $this->buildOpenApiResponse($existingResponses, '400', 'Invalid input', $openapiOperation); + if (!isset($existingResponses[400])) { + $openapiOperation = $openapiOperation->withResponse(400, new Response('Invalid input')); + } + $openapiOperation = $this->buildOpenApiResponse($existingResponses, '422', 'Unprocessable entity', $openapiOperation); break; case HttpOperation::METHOD_DELETE: $successStatus = (string) $operation->getStatus() ?: 204; - $openapiOperation = $openapiOperation->withResponse($successStatus, new Response(sprintf('%s resource deleted', $resourceShortName))); + + $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s resource deleted', $resourceShortName), $openapiOperation); + break; } if (!$operation instanceof CollectionOperationInterface && HttpOperation::METHOD_POST !== $operation->getMethod()) { - $openapiOperation = $openapiOperation->withResponse(404, new Response('Resource not found')); + if (!isset($existingResponses[404])) { + $openapiOperation = $openapiOperation->withResponse(404, new Response('Resource not found')); + } } if (!$openapiOperation->getResponses()) { @@ -377,6 +383,22 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } } + private function buildOpenApiResponse(array $existingResponses, int|string $status, string $description, Model\Operation $openapiOperation = null, HttpOperation $operation = null, array $responseMimeTypes = null, array $operationOutputSchemas = null, ResourceMetadataCollection $resourceMetadataCollection = null): Model\Operation + { + if (isset($existingResponses[$status])) { + return $openapiOperation; + } + $responseLinks = $responseContent = null; + if ($responseMimeTypes && $operationOutputSchemas) { + $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas); + } + if ($resourceMetadataCollection && $operation) { + $responseLinks = $this->getLinks($resourceMetadataCollection, $operation); + } + + return $openapiOperation->withResponse($status, new Response($description, $responseContent, null, $responseLinks)); + } + /** * @return \ArrayObject */ diff --git a/tests/OpenApi/Factory/OpenApiFactoryTest.php b/tests/OpenApi/Factory/OpenApiFactoryTest.php index 424a3757d7b..636994ea1a0 100644 --- a/tests/OpenApi/Factory/OpenApiFactoryTest.php +++ b/tests/OpenApi/Factory/OpenApiFactoryTest.php @@ -190,6 +190,55 @@ public function testInvoke(): void ]), ), )), + 'putDummyItemWithResponse' => (new Put())->withUriTemplate('/dummyitems/{id}')->withOperation($baseOperation)->withOpenapi(new OpenApiOperation( + responses: [ + '200' => new OpenApiResponse( + description: 'Success', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ]), + headers: new \ArrayObject([ + 'API_KEY' => ['description' => 'Api Key', 'schema' => ['type' => 'string']], + ]), + links: new \ArrayObject([ + 'link' => ['$ref' => '#/components/schemas/Dummy'], + ]), + ), + '400' => new OpenApiResponse( + description: 'Error', + ), + ], + )), + 'getDummyItemImageCollection' => (new GetCollection())->withUriTemplate('/dummyitems/{id}/images')->withOperation($baseOperation)->withOpenapi(new OpenApiOperation( + responses: [ + '200' => new OpenApiResponse( + description: 'Success', + ), + ], + )), + 'postDummyItemWithResponse' => (new Post())->withUriTemplate('/dummyitems')->withOperation($baseOperation)->withOpenapi(new OpenApiOperation( + responses: [ + '201' => new OpenApiResponse( + description: 'Created', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ]), + headers: new \ArrayObject([ + 'API_KEY' => ['description' => 'Api Key', 'schema' => ['type' => 'string']], + ]), + links: new \ArrayObject([ + 'link' => ['$ref' => '#/components/schemas/Dummy'], + ]), + ), + '400' => new OpenApiResponse( + description: 'Error', + ), + ], + )), ]) ); @@ -694,5 +743,107 @@ public function testInvoke(): void ), deprecated: false, ), $requestBodyPath->getPost()); + + $dummyItemPath = $paths->getPath('/dummyitems/{id}'); + $this->assertEquals(new Operation( + 'putDummyItemWithResponse', + ['Dummy'], + [ + '200' => new Response( + 'Success', + new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ]), + new \ArrayObject([ + 'API_KEY' => ['description' => 'Api Key', 'schema' => ['type' => 'string']], + ]), + new \ArrayObject([ + 'link' => ['$ref' => '#/components/schemas/Dummy'], + ]) + ), + '400' => new Response('Error'), + '422' => new Response('Unprocessable entity'), + '404' => new Response('Resource not found'), + ], + 'Replaces the Dummy resource.', + 'Replaces the Dummy resource.', + null, + [], + new RequestBody( + 'The updated Dummy resource', + new \ArrayObject([ + 'application/ld+json' => new MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])), + ]), + true + ), + deprecated: false + ), $dummyItemPath->getPut()); + + $dummyItemPath = $paths->getPath('/dummyitems'); + $this->assertEquals(new Operation( + 'postDummyItemWithResponse', + ['Dummy'], + [ + '201' => new Response( + 'Created', + new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ]), + new \ArrayObject([ + 'API_KEY' => ['description' => 'Api Key', 'schema' => ['type' => 'string']], + ]), + new \ArrayObject([ + 'link' => ['$ref' => '#/components/schemas/Dummy'], + ]) + ), + '400' => new Response('Error'), + '422' => new Response('Unprocessable entity'), + ], + 'Creates a Dummy resource.', + 'Creates a Dummy resource.', + null, + [], + new RequestBody( + 'The new Dummy resource', + new \ArrayObject([ + 'application/ld+json' => new MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])), + ]), + true + ), + deprecated: false + ), $dummyItemPath->getPost()); + + $dummyItemPath = $paths->getPath('/dummyitems/{id}/images'); + + $this->assertEquals(new Operation( + 'getDummyItemImageCollection', + ['Dummy'], + [ + '200' => new Response( + 'Success' + ), + ], + 'Retrieves the collection of Dummy resources.', + 'Retrieves the collection of Dummy resources.', + null, + [ + new Parameter('page', 'query', 'The collection page number', false, false, true, [ + 'type' => 'integer', + 'default' => 1, + ]), + new Parameter('itemsPerPage', 'query', 'The number of items per page', false, false, true, [ + 'type' => 'integer', + 'default' => 30, + 'minimum' => 0, + ]), + new Parameter('pagination', 'query', 'Enable or disable pagination', false, false, true, [ + 'type' => 'boolean', + ]), + ] + ), $dummyItemPath->getGet()); } }