Skip to content

Commit dcab6c7

Browse files
authored
fix(jsonschema): keep integer and number properties draft 4 compliant (#6098)
1 parent a96220c commit dcab6c7

File tree

5 files changed

+177
-2
lines changed

5 files changed

+177
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\JsonSchema;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
18+
/**
19+
* This factory decorates range integer and number properties to keep Draft 4 backward compatibility.
20+
*
21+
* @see https://github.com/api-platform/core/issues/6041
22+
*
23+
* @internal
24+
*/
25+
final class BackwardCompatibleSchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
26+
{
27+
public const SCHEMA_DRAFT4_VERSION = 'draft_4';
28+
29+
public function __construct(private readonly SchemaFactoryInterface $decorated)
30+
{
31+
}
32+
33+
/**
34+
* {@inheritDoc}
35+
*/
36+
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
37+
{
38+
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
39+
40+
if (!($serializerContext[self::SCHEMA_DRAFT4_VERSION] ?? false)) {
41+
return $schema;
42+
}
43+
44+
foreach ($schema->getDefinitions() as $definition) {
45+
foreach ($definition['properties'] ?? [] as $property) {
46+
if (isset($property['type']) && \in_array($property['type'], ['integer', 'number'], true)) {
47+
if (isset($property['exclusiveMinimum'])) {
48+
$property['minimum'] = $property['exclusiveMinimum'];
49+
$property['exclusiveMinimum'] = true;
50+
}
51+
if (isset($property['exclusiveMaximum'])) {
52+
$property['maximum'] = $property['exclusiveMaximum'];
53+
$property['exclusiveMaximum'] = true;
54+
}
55+
}
56+
}
57+
}
58+
59+
return $schema;
60+
}
61+
62+
public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
63+
{
64+
if ($this->decorated instanceof SchemaFactoryAwareInterface) {
65+
$this->decorated->setSchemaFactory($schemaFactory);
66+
}
67+
}
68+
}

src/Symfony/Bundle/Resources/config/json_schema.xml

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
<argument type="service" id="api_platform.json_schema.metadata.property.metadata_factory.schema.inner" />
3737
</service>
3838

39+
<service id="api_platform.json_schema.backward_compatible_schema_factory" decorates="api_platform.json_schema.schema_factory" decoration-priority="-2" class="ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory">
40+
<argument type="service" id="api_platform.json_schema.backward_compatible_schema_factory.inner" />
41+
</service>
42+
3943
</services>
4044

4145
</container>

src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Symfony\Bundle\Test;
1515

16+
use ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory;
1617
use ApiPlatform\JsonSchema\Schema;
1718
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
1819
use ApiPlatform\Metadata\Get;
@@ -118,7 +119,7 @@ public static function assertMatchesResourceCollectionJsonSchema(string $resourc
118119
$operation = $operationName ? (new GetCollection())->withName($operationName) : new GetCollection();
119120
}
120121

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

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

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

138139
static::assertMatchesJsonSchema($schema->getArrayCopy());
139140
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6041;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\GetCollection;
19+
use Doctrine\ORM\Mapping as ORM;
20+
use Symfony\Component\Validator\Constraints as Assert;
21+
22+
#[ApiResource(operations: [
23+
new Get(uriTemplate: 'numeric-validated/{id}'),
24+
new GetCollection(uriTemplate: 'numeric-validated'),
25+
])]
26+
#[ORM\Entity]
27+
class NumericValidated
28+
{
29+
#[ORM\Column(type: 'integer')]
30+
#[ORM\Id]
31+
#[ORM\GeneratedValue(strategy: 'AUTO')]
32+
private ?int $id = null;
33+
34+
#[Assert\Range(min: 1, max: 10)]
35+
#[ORM\Column]
36+
public int $range;
37+
38+
#[Assert\GreaterThan(value: 10)]
39+
#[ORM\Column]
40+
public int $greaterThanMe;
41+
42+
#[Assert\GreaterThanOrEqual(value: '10.99')]
43+
#[ORM\Column]
44+
public float $greaterThanOrEqualToMe;
45+
46+
#[Assert\LessThan(value: 99)]
47+
#[ORM\Column]
48+
public int $lessThanMe;
49+
50+
#[Assert\LessThanOrEqual(value: '99.33')]
51+
#[ORM\Column]
52+
public float $lessThanOrEqualToMe;
53+
54+
#[Assert\Positive]
55+
#[ORM\Column]
56+
public int $positive;
57+
58+
#[Assert\PositiveOrZero]
59+
#[ORM\Column]
60+
public int $positiveOrZero;
61+
62+
#[Assert\Negative]
63+
#[ORM\Column]
64+
public int $negative;
65+
66+
#[Assert\NegativeOrZero]
67+
#[ORM\Column]
68+
public int $negativeOrZero;
69+
70+
public function getId(): ?int
71+
{
72+
return $this->id;
73+
}
74+
}

tests/Symfony/Bundle/Test/ApiTestCaseTest.php

+28
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
1818
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
1919
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6041\NumericValidated;
2021
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonSchemaContextDummy;
2122
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User;
2223
use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface;
@@ -180,6 +181,33 @@ public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithCo
180181
$this->assertMatchesResourceCollectionJsonSchema(User::class, null, 'jsonld', ['groups' => ['api-test-case-group']]);
181182
}
182183

184+
public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithRangeAssertions(): void
185+
{
186+
$this->recreateSchema();
187+
188+
/** @var EntityManagerInterface $manager */
189+
$manager = static::getContainer()->get('doctrine')->getManager();
190+
$numericValidated = new NumericValidated();
191+
$numericValidated->range = 5;
192+
$numericValidated->greaterThanMe = 11;
193+
$numericValidated->greaterThanOrEqualToMe = 10.99;
194+
$numericValidated->lessThanMe = 11;
195+
$numericValidated->lessThanOrEqualToMe = 99.33;
196+
$numericValidated->positive = 1;
197+
$numericValidated->positiveOrZero = 0;
198+
$numericValidated->negative = -1;
199+
$numericValidated->negativeOrZero = 0;
200+
201+
$manager->persist($numericValidated);
202+
$manager->flush();
203+
204+
self::createClient()->request('GET', "/numeric-validated/{$numericValidated->getId()}");
205+
$this->assertMatchesResourceItemJsonSchema(NumericValidated::class);
206+
207+
self::createClient()->request('GET', '/numeric-validated');
208+
$this->assertMatchesResourceCollectionJsonSchema(NumericValidated::class);
209+
}
210+
183211
// Next tests have been imported from dms/phpunit-arraysubset-asserts, because the original constraint has been deprecated.
184212

185213
public function testAssertArraySubsetPassesStrictConfig(): void

0 commit comments

Comments
 (0)