From 4ce84347f9269a444f5d9d73cc17a46f56675a24 Mon Sep 17 00:00:00 2001 From: Zowac Date: Thu, 16 Mar 2023 16:28:49 +0100 Subject: [PATCH] fix(jsonschema): find the related operation instead of assuming one --- features/openapi/docs.feature | 21 +++++++ src/JsonSchema/SchemaFactory.php | 60 ++++++++++++------- .../Resource/ResourceMetadataCollection.php | 8 +-- .../TestBundle/Entity/JsonSchemaResource.php | 29 +++++++++ .../Entity/JsonSchemaResourceRelated.php | 26 ++++++++ 5 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Entity/JsonSchemaResource.php create mode 100644 tests/Fixtures/TestBundle/Entity/JsonSchemaResourceRelated.php diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 1ec6b74ba78..55d8b642725 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -299,3 +299,24 @@ Feature: Documentation support And the JSON node "components.schemas.RamseyUuidDummy.properties.id.description" should be equal to "The dummy id." And the JSON node "components.schemas.RelatedDummy-barcelona" should not exist And the JSON node "components.schemas.RelatedDummybarcelona" should exist + + @!mongodb + Scenario: Retrieve the OpenAPI documentation to see if shortName property is used + Given I send a "GET" request to "/docs.json" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json; charset=utf-8" + And the OpenAPI class "Resource" exists + And the OpenAPI class "ResourceRelated" exists + And the "resourceRelated" property for the OpenAPI class "Resource" should be equal to: + """ + { + "readOnly":true, + "anyOf":[ + { + "$ref":"#/components/schemas/ResourceRelated" + } + ], + "nullable":true + } + """ diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 41a39e5e5e5..326f78e2b25 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -14,6 +14,7 @@ namespace ApiPlatform\JsonSchema; use ApiPlatform\Api\ResourceClassResolverInterface; +use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\HttpOperation; @@ -21,6 +22,7 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\OpenApi\Factory\OpenApiFactory; use ApiPlatform\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyInfo\Type; @@ -269,30 +271,24 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP ]; } - // The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise - if (!$operation || !$operation->getClass()) { + if (null === $operation) { $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); + try { + $operation = $resourceMetadataCollection->getOperation(); + } catch (OperationNotFoundException $e) { + $operation = new HttpOperation(); + } - if ($operation && $operation->getName()) { - $operation = $resourceMetadataCollection->getOperation($operation->getName()); - } else { - // Guess the operation and use the first one that matches criterias - foreach ($resourceMetadataCollection as $resourceMetadata) { - foreach ($resourceMetadata->getOperations() ?? [] as $op) { - if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { - $operation = $op; - break 2; - } - - if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { - $operation = $op; - break 2; - } - - if (!$operation) { - $operation = new HttpOperation(); - } - } + $operation = $this->findOperationForType($resourceMetadataCollection, $type, $operation); + } else { + // The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise + if (!$operation->getClass()) { + $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); + + if ($operation->getName()) { + $operation = $resourceMetadataCollection->getOperation($operation->getName()); + } else { + $operation = $this->findOperationForType($resourceMetadataCollection, $type, $operation); } } } @@ -320,6 +316,26 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP ]; } + private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation) + { + // Find the operation and use the first one that matches criterias + foreach ($resourceMetadataCollection as $resourceMetadata) { + foreach ($resourceMetadata->getOperations() ?? [] as $op) { + if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { + $operation = $op; + break 2; + } + + if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { + $operation = $op; + break 2; + } + } + } + + return $operation; + } + private function getSerializerContext(Operation $operation, string $type = Schema::TYPE_OUTPUT): array { return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); diff --git a/src/Metadata/Resource/ResourceMetadataCollection.php b/src/Metadata/Resource/ResourceMetadataCollection.php index 5260095bced..775638cd543 100644 --- a/src/Metadata/Resource/ResourceMetadataCollection.php +++ b/src/Metadata/Resource/ResourceMetadataCollection.php @@ -89,9 +89,9 @@ public function getOperation(?string $operationName = null, bool $forceCollectio } // Idea: - // if ($metadata) { - // return (new class extends HttpOperation {})->withResource($metadata); - // } +// if ($metadata) { +// return (new class extends HttpOperation {})->withResource($metadata); +// } $this->handleNotFound($operationName, $metadata); } @@ -102,7 +102,7 @@ public function getOperation(?string $operationName = null, bool $forceCollectio private function handleNotFound(string $operationName, ?ApiResource $metadata): void { // Hide the FQDN in the exception message if possible - $shortName = $metadata ? $metadata->getShortName() : $this->resourceClass; + $shortName = $metadata?->getShortName() ? $metadata->getShortName() : $this->resourceClass; if (!$metadata && false !== $pos = strrpos($shortName, '\\')) { $shortName = substr($shortName, $pos + 1); } diff --git a/tests/Fixtures/TestBundle/Entity/JsonSchemaResource.php b/tests/Fixtures/TestBundle/Entity/JsonSchemaResource.php new file mode 100644 index 00000000000..b6eba37f58f --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/JsonSchemaResource.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; + +#[ApiResource( + shortName: 'Resource', +)] +class JsonSchemaResource +{ + #[ApiProperty(identifier: true)] + public $id; + + #[ApiProperty(writable: false, readableLink: true)] + public ?JsonSchemaResourceRelated $resourceRelated = null; +} diff --git a/tests/Fixtures/TestBundle/Entity/JsonSchemaResourceRelated.php b/tests/Fixtures/TestBundle/Entity/JsonSchemaResourceRelated.php new file mode 100644 index 00000000000..cac2d3a3407 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/JsonSchemaResourceRelated.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; + +#[ApiResource( + shortName: 'ResourceRelated', +)] +class JsonSchemaResourceRelated +{ + #[ApiProperty(identifier: true)] + public $id; +}