Skip to content

Commit

Permalink
fix(jsonschema): access related subschema on readableLink
Browse files Browse the repository at this point in the history
fixes api-platform#5501

The locations relation inside BrokenDocs is a Resource (named Related) but its only operation is a NotExposed. Still, serializer groups are set, and therefore it is a "readableLink" so we actually want to compute the schema, even if it's not accessible directly, it is accessible through that relation.
  • Loading branch information
soyuka committed Mar 31, 2023
1 parent f128e3b commit 72309ef
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 4 deletions.
13 changes: 9 additions & 4 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5501\Related;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
Expand All @@ -38,6 +39,8 @@ final class SchemaFactory implements SchemaFactoryInterface
use ResourceClassInfoTrait;
private array $distinctFormats = [];

// Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object
public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link';
public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name';

public function __construct(private readonly TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null)
Expand Down Expand Up @@ -300,24 +303,26 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP
$inputOrOutput = Schema::TYPE_OUTPUT === $type ? ($operation->getOutput() ?? $inputOrOutput) : ($operation->getInput() ?? $inputOrOutput);
}

if (null === ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null)) {
$outputClass = ($serializerContext[self::FORCE_SUBSCHEMA] ?? false) ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null);

if (null === $outputClass) {
// input or output disabled
return null;
}

if (!$operation) {
return [$operation, $serializerContext ?? [], [], $inputOrOutput['class'] ?? $inputOrOutput->class];
return [$operation, $serializerContext ?? [], [], $outputClass];
}

return [
$operation,
$serializerContext ?? $this->getSerializerContext($operation, $type),
$this->getValidationGroups($operation),
$inputOrOutput['class'] ?? $inputOrOutput->class,
$outputClass,
];
}

private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation)
private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation
{
// Find the operation and use the first one that matches criterias
foreach ($resourceMetadataCollection as $resourceMetadata) {
Expand Down
1 change: 1 addition & 0 deletions src/JsonSchema/TypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ private function getClassType(?string $className, bool $nullable, string $format
throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.');
}

$serializerContext += [SchemaFactory::FORCE_SUBSCHEMA => true];
$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, $subSchema, $serializerContext, false);

return ['$ref' => $subSchema['$ref']];
Expand Down
37 changes: 37 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/Issue5501/BrokenDocs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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\ApiResource\Issue5501;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
operations: [
new Get(
normalizationContext: ['groups' => ['location:read_collection']]
),
]
)]
class BrokenDocs
{
public ?int $id = null;

/**
* @var ?ArrayCollection<Related>
*/
#[Groups(['location:write', 'location:read_collection'])]
public ?ArrayCollection $locations;
}
24 changes: 24 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/Issue5501/Related.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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\ApiResource\Issue5501;

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(operations: [])]
class Related
{
#[Groups(['location:write', 'location:read_collection'])]
public ?string $name = null;
}
13 changes: 13 additions & 0 deletions tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,17 @@ public function testExecuteWithJsonldTypeInput(): void
$this->assertStringNotContainsString('@context', $result);
$this->assertStringNotContainsString('@type', $result);
}

/**
* Test issue #5501, the locations relation inside BrokenDocs is a Resource (named Related) but its only operation is a NotExposed.
* Still, serializer groups are set, and therefore it is a "readableLink" so we actually want to compute the schema, even if it's not accessible
* directly, it is accessible through that relation.
*/
public function testExecuteWithNotExposedResourceAndReadableLink(): void
{
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5501\BrokenDocs', '--type' => 'output']);
$result = $this->tester->getDisplay();

$this->assertStringContainsString('Related.jsonld-location.read_collection', $result);
}
}

0 comments on commit 72309ef

Please sign in to comment.