Skip to content

Commit

Permalink
fix(jsonschema): keep integer and number properties draft 4 compliant (
Browse files Browse the repository at this point in the history
  • Loading branch information
llupa authored Jan 16, 2024
1 parent a96220c commit dcab6c7
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 2 deletions.
68 changes: 68 additions & 0 deletions src/JsonSchema/BackwardCompatibleSchemaFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?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\JsonSchema;

use ApiPlatform\Metadata\Operation;

/**
* This factory decorates range integer and number properties to keep Draft 4 backward compatibility.
*
* @see https://github.com/api-platform/core/issues/6041
*
* @internal
*/
final class BackwardCompatibleSchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
public const SCHEMA_DRAFT4_VERSION = 'draft_4';

public function __construct(private readonly SchemaFactoryInterface $decorated)
{
}

/**
* {@inheritDoc}
*/
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, Operation $operation = null, Schema $schema = null, array $serializerContext = null, bool $forceCollection = false): Schema
{
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);

if (!($serializerContext[self::SCHEMA_DRAFT4_VERSION] ?? false)) {
return $schema;
}

foreach ($schema->getDefinitions() as $definition) {
foreach ($definition['properties'] ?? [] as $property) {
if (isset($property['type']) && \in_array($property['type'], ['integer', 'number'], true)) {
if (isset($property['exclusiveMinimum'])) {
$property['minimum'] = $property['exclusiveMinimum'];
$property['exclusiveMinimum'] = true;
}
if (isset($property['exclusiveMaximum'])) {
$property['maximum'] = $property['exclusiveMaximum'];
$property['exclusiveMaximum'] = true;
}
}
}
}

return $schema;
}

public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
{
if ($this->decorated instanceof SchemaFactoryAwareInterface) {
$this->decorated->setSchemaFactory($schemaFactory);
}
}
}
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/Resources/config/json_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<argument type="service" id="api_platform.json_schema.metadata.property.metadata_factory.schema.inner" />
</service>

<service id="api_platform.json_schema.backward_compatible_schema_factory" decorates="api_platform.json_schema.schema_factory" decoration-priority="-2" class="ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory">
<argument type="service" id="api_platform.json_schema.backward_compatible_schema_factory.inner" />
</service>

</services>

</container>
5 changes: 3 additions & 2 deletions src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Symfony\Bundle\Test;

use ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory;
use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\Metadata\Get;
Expand Down Expand Up @@ -118,7 +119,7 @@ public static function assertMatchesResourceCollectionJsonSchema(string $resourc
$operation = $operationName ? (new GetCollection())->withName($operationName) : new GetCollection();
}

$schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, $serializationContext);
$schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, ($serializationContext ?? []) + [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);

static::assertMatchesJsonSchema($schema->getArrayCopy());
}
Expand All @@ -133,7 +134,7 @@ public static function assertMatchesResourceItemJsonSchema(string $resourceClass
$operation = $operationName ? (new Get())->withName($operationName) : new Get();
}

$schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, $serializationContext);
$schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, ($serializationContext ?? []) + [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);

static::assertMatchesJsonSchema($schema->getArrayCopy());
}
Expand Down
74 changes: 74 additions & 0 deletions tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?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\Issue6041;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(operations: [
new Get(uriTemplate: 'numeric-validated/{id}'),
new GetCollection(uriTemplate: 'numeric-validated'),
])]
#[ORM\Entity]
class NumericValidated
{
#[ORM\Column(type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;

#[Assert\Range(min: 1, max: 10)]
#[ORM\Column]
public int $range;

#[Assert\GreaterThan(value: 10)]
#[ORM\Column]
public int $greaterThanMe;

#[Assert\GreaterThanOrEqual(value: '10.99')]
#[ORM\Column]
public float $greaterThanOrEqualToMe;

#[Assert\LessThan(value: 99)]
#[ORM\Column]
public int $lessThanMe;

#[Assert\LessThanOrEqual(value: '99.33')]
#[ORM\Column]
public float $lessThanOrEqualToMe;

#[Assert\Positive]
#[ORM\Column]
public int $positive;

#[Assert\PositiveOrZero]
#[ORM\Column]
public int $positiveOrZero;

#[Assert\Negative]
#[ORM\Column]
public int $negative;

#[Assert\NegativeOrZero]
#[ORM\Column]
public int $negativeOrZero;

public function getId(): ?int
{
return $this->id;
}
}
28 changes: 28 additions & 0 deletions tests/Symfony/Bundle/Test/ApiTestCaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6041\NumericValidated;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonSchemaContextDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User;
use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface;
Expand Down Expand Up @@ -180,6 +181,33 @@ public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithCo
$this->assertMatchesResourceCollectionJsonSchema(User::class, null, 'jsonld', ['groups' => ['api-test-case-group']]);
}

public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithRangeAssertions(): void
{
$this->recreateSchema();

/** @var EntityManagerInterface $manager */
$manager = static::getContainer()->get('doctrine')->getManager();
$numericValidated = new NumericValidated();
$numericValidated->range = 5;
$numericValidated->greaterThanMe = 11;
$numericValidated->greaterThanOrEqualToMe = 10.99;
$numericValidated->lessThanMe = 11;
$numericValidated->lessThanOrEqualToMe = 99.33;
$numericValidated->positive = 1;
$numericValidated->positiveOrZero = 0;
$numericValidated->negative = -1;
$numericValidated->negativeOrZero = 0;

$manager->persist($numericValidated);
$manager->flush();

self::createClient()->request('GET', "/numeric-validated/{$numericValidated->getId()}");
$this->assertMatchesResourceItemJsonSchema(NumericValidated::class);

self::createClient()->request('GET', '/numeric-validated');
$this->assertMatchesResourceCollectionJsonSchema(NumericValidated::class);
}

// Next tests have been imported from dms/phpunit-arraysubset-asserts, because the original constraint has been deprecated.

public function testAssertArraySubsetPassesStrictConfig(): void
Expand Down

0 comments on commit dcab6c7

Please sign in to comment.