Skip to content

Commit

Permalink
fix(jsonschema): find the related operation instead of assuming one (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Zowac authored Mar 21, 2023
1 parent dd94591 commit e471622
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 26 deletions.
21 changes: 21 additions & 0 deletions features/openapi/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
"""
60 changes: 38 additions & 22 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
namespace ApiPlatform\JsonSchema;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Exception\OperationNotFoundException;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
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;
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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() ?? []);
Expand Down
8 changes: 4 additions & 4 deletions src/Metadata/Resource/ResourceMetadataCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
29 changes: 29 additions & 0 deletions tests/Fixtures/TestBundle/Entity/JsonSchemaResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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;
}
26 changes: 26 additions & 0 deletions tests/Fixtures/TestBundle/Entity/JsonSchemaResourceRelated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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;
}

0 comments on commit e471622

Please sign in to comment.