From 8b57125dba5e11a2a9e1bba86d1b8ced1a70a629 Mon Sep 17 00:00:00 2001 From: Benjamin Rothan Date: Sat, 18 May 2024 23:31:31 +0200 Subject: [PATCH] feat(openapi): allow optional request body content --- src/OpenApi/Factory/OpenApiFactory.php | 22 ++++++--- src/OpenApi/Model/RequestBody.php | 6 +-- .../Tests/Factory/OpenApiFactoryTest.php | 45 ++++++++++++++++--- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index b059d9c183d..12148ed4aad 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -394,17 +394,25 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection ); $openapiOperation = $openapiOperation->withRequestBody(new RequestBody($contextRequestBody['description'] ?? '', new \ArrayObject($contextRequestBody['content']), $contextRequestBody['required'] ?? false)); } elseif ( - null === $openapiOperation->getRequestBody() && \in_array($method, ['PATCH', 'PUT', 'POST'], true) + \in_array($method, ['PATCH', 'PUT', 'POST'], true) && !(false === ($input = $operation->getInput()) || (\is_array($input) && null === $input['class'])) ) { - $operationInputSchemas = []; - foreach ($requestMimeTypes as $operationFormat) { - $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operation, $schema, null, $forceSchemaCollection); - $operationInputSchemas[$operationFormat] = $operationInputSchema; - $this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions()); + $content = $openapiOperation->getRequestBody()?->getContent(); + if (null === $content) { + $operationInputSchemas = []; + foreach ($requestMimeTypes as $operationFormat) { + $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operation, $schema, null, $forceSchemaCollection); + $operationInputSchemas[$operationFormat] = $operationInputSchema; + $this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions()); + } + $content = $this->buildContent($requestMimeTypes, $operationInputSchemas); } - $openapiOperation = $openapiOperation->withRequestBody(new RequestBody(sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName), $this->buildContent($requestMimeTypes, $operationInputSchemas), true)); + $openapiOperation = $openapiOperation->withRequestBody(new RequestBody( + description: $openapiOperation->getRequestBody()?->getDescription() ?? sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName), + content: $content, + required: $openapiOperation->getRequestBody()?->getRequired() ?? true, + )); } // TODO Remove in 4.0 diff --git a/src/OpenApi/Model/RequestBody.php b/src/OpenApi/Model/RequestBody.php index b1f452f814d..6d85b4d07cb 100644 --- a/src/OpenApi/Model/RequestBody.php +++ b/src/OpenApi/Model/RequestBody.php @@ -17,16 +17,16 @@ final class RequestBody { use ExtensionTrait; - public function __construct(private string $description = '', private ?\ArrayObject $content = null, private bool $required = false) + public function __construct(private ?string $description = null, private ?\ArrayObject $content = null, private bool $required = false) { } - public function getDescription(): string + public function getDescription(): ?string { return $this->description; } - public function getContent(): \ArrayObject + public function getContent(): ?\ArrayObject { return $this->content; } diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 7be076f3043..924032e2a60 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -175,11 +175,11 @@ public function testInvoke(): void 'filteredDummyCollection' => (new GetCollection())->withUriTemplate('/filtered')->withFilters(['f1', 'f2', 'f3', 'f4', 'f5'])->withOperation($baseOperation), // Paginated 'paginatedDummyCollection' => (new GetCollection())->withUriTemplate('/paginated') - ->withPaginationClientEnabled(true) - ->withPaginationClientItemsPerPage(true) - ->withPaginationItemsPerPage(20) - ->withPaginationMaximumItemsPerPage(80) - ->withOperation($baseOperation), + ->withPaginationClientEnabled(true) + ->withPaginationClientItemsPerPage(true) + ->withPaginationItemsPerPage(20) + ->withPaginationMaximumItemsPerPage(80) + ->withOperation($baseOperation), 'postDummyCollectionWithRequestBody' => (new Post())->withUriTemplate('/dummiesRequestBody')->withOperation($baseOperation)->withOpenapi(new OpenApiOperation( requestBody: new RequestBody( description: 'List of Ids', @@ -202,6 +202,11 @@ public function testInvoke(): void ]), ), )), + 'postDummyCollectionWithRequestBodyWithoutContent' => (new Post())->withUriTemplate('/dummiesRequestBodyWithoutContent')->withOperation($baseOperation)->withOpenapi(new OpenApiOperation( + requestBody: new RequestBody( + description: 'Extended description for the new Dummy resource', + ), + )), 'putDummyItemWithResponse' => (new Put())->withUriTemplate('/dummyitems/{id}')->withOperation($baseOperation)->withOpenapi(new OpenApiOperation( responses: [ '200' => new OpenApiResponse( @@ -872,6 +877,36 @@ public function testInvoke(): void deprecated: false, ), $requestBodyPath->getPost()); + $requestBodyPath = $paths->getPath('/dummiesRequestBodyWithoutContent'); + $this->assertEquals(new Operation( + 'postDummyCollectionWithRequestBodyWithoutContent', + ['Dummy'], + [ + '201' => new Response( + 'Dummy resource created', + new \ArrayObject([ + 'application/ld+json' => new MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Dummy.OutputDto']))), + ]), + null, + new \ArrayObject(['getDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), null, 'This is a dummy')]) + ), + '400' => new Response('Invalid input'), + '422' => new Response('Unprocessable entity'), + ], + 'Creates a Dummy resource.', + 'Creates a Dummy resource.', + null, + [], + new RequestBody( + 'Extended description for the new Dummy resource', + new \ArrayObject([ + 'application/ld+json' => new MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Dummy']))), + ]), + false + ), + deprecated: false, + ), $requestBodyPath->getPost()); + $dummyItemPath = $paths->getPath('/dummyitems/{id}'); $this->assertEquals(new Operation( 'putDummyItemWithResponse',