diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d782466bd6..2722e125372 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## 2.7.0
+* JSON Schema: Add support for generating property schema with Range restriction (#4158)
* **BC**: Change `api_platform.listener.request.add_format` priority from 7 to 28 to execute it before firewall (priority 8) (#3599)
* **BC**: Use `@final` annotation in ORM filters (#4109)
* Allow defining `exception_to_status` per operation (#3519)
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
index 77455124011..9a743ef093c 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
@@ -26,6 +26,10 @@
+
+
+
+
diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestriction.php b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestriction.php
new file mode 100644
index 00000000000..00a1ca5875d
--- /dev/null
+++ b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestriction.php
@@ -0,0 +1,51 @@
+
+ *
+ * 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\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction;
+
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\Range;
+
+/**
+ * @author Tomas Norkūnas
+ */
+final class PropertySchemaRangeRestriction implements PropertySchemaRestrictionMetadataInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array
+ {
+ $restriction = [];
+
+ if (isset($constraint->min) && is_numeric($constraint->min)) {
+ $restriction['minimum'] = $constraint->min;
+ }
+
+ if (isset($constraint->max) && is_numeric($constraint->max)) {
+ $restriction['maximum'] = $constraint->max;
+ }
+
+ return $restriction;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool
+ {
+ return $constraint instanceof Range && null !== ($type = $propertyMetadata->getType()) && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true);
+ }
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 4a9c065f3b9..c205c32928a 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -1337,6 +1337,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
'api_platform.metadata.property.metadata_factory.validator',
'api_platform.metadata.property_schema.length_restriction',
'api_platform.metadata.property_schema.one_of_restriction',
+ 'api_platform.metadata.property_schema.range_restriction',
'api_platform.metadata.property_schema.regex_restriction',
'api_platform.metadata.property_schema.format_restriction',
'api_platform.metadata.property.metadata_factory.yaml',
diff --git a/tests/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php b/tests/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php
new file mode 100644
index 00000000000..3891d6c1d60
--- /dev/null
+++ b/tests/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php
@@ -0,0 +1,72 @@
+
+ *
+ * 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\Core\Tests\Bridge\Symfony\Validator\Metadata\Property\Restriction;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRangeRestriction;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Tests\ProphecyTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\Length;
+use Symfony\Component\Validator\Constraints\Range;
+
+/**
+ * @author Tomas Norkūnas
+ */
+final class PropertySchemaRangeRestrictionTest extends TestCase
+{
+ use ProphecyTrait;
+
+ private $propertySchemaRangeRestriction;
+
+ protected function setUp(): void
+ {
+ $this->propertySchemaRangeRestriction = new PropertySchemaRangeRestriction();
+ }
+
+ /**
+ * @dataProvider supportsProvider
+ */
+ public function testSupports(Constraint $constraint, PropertyMetadata $propertyMetadata, bool $expectedResult): void
+ {
+ self::assertSame($expectedResult, $this->propertySchemaRangeRestriction->supports($constraint, $propertyMetadata));
+ }
+
+ public function supportsProvider(): \Generator
+ {
+ yield 'supported int' => [new Range(['min' => 1, 'max' => 10]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT)), true];
+ yield 'supported float' => [new Range(['min' => 1, 'max' => 10]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_FLOAT)), true];
+
+ yield 'not supported constraint' => [new Length(['min' => 1]), new PropertyMetadata(), false];
+ yield 'not supported type' => [new Range(['min' => 1]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)), false];
+ }
+
+ /**
+ * @dataProvider createProvider
+ */
+ public function testCreate(Constraint $constraint, PropertyMetadata $propertyMetadata, array $expectedResult): void
+ {
+ self::assertSame($expectedResult, $this->propertySchemaRangeRestriction->create($constraint, $propertyMetadata));
+ }
+
+ public function createProvider(): \Generator
+ {
+ yield 'int min' => [new Range(['min' => 1]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT)), ['minimum' => 1]];
+ yield 'int max' => [new Range(['max' => 10]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT)), ['maximum' => 10]];
+
+ yield 'float min' => [new Range(['min' => 1.5]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_FLOAT)), ['minimum' => 1.5]];
+ yield 'float max' => [new Range(['max' => 10.5]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_FLOAT)), ['maximum' => 10.5]];
+ }
+}
diff --git a/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php b/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
index f7c59d07666..5a05fcc8c07 100644
--- a/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
+++ b/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
@@ -16,12 +16,14 @@
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaFormat;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaLengthRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaOneOfRestriction;
+use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRangeRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRegexRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Tests\Fixtures\DummyAtLeastOneOfValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyIriWithValidationEntity;
+use ApiPlatform\Core\Tests\Fixtures\DummyRangeValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummySequentiallyValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyValidatedEntity;
use ApiPlatform\Core\Tests\ProphecyTrait;
@@ -406,4 +408,41 @@ public function testCreateWithAtLeastOneOfConstraint(): void
['minLength' => 10],
], $schema['oneOf']);
}
+
+ /**
+ * @dataProvider provideRangeConstraintCases
+ */
+ public function testCreateWithRangeConstraint(Type $type, string $property, array $expectedSchema): void
+ {
+ $validatorClassMetadata = new ClassMetadata(DummyRangeValidatedEntity::class);
+ (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata);
+
+ $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class);
+ $validatorMetadataFactory->getMetadataFor(DummyRangeValidatedEntity::class)
+ ->willReturn($validatorClassMetadata)
+ ->shouldBeCalled();
+
+ $decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $decoratedPropertyMetadataFactory->create(DummyRangeValidatedEntity::class, $property, [])->willReturn(
+ new PropertyMetadata($type)
+ )->shouldBeCalled();
+ $validationPropertyMetadataFactory = new ValidatorPropertyMetadataFactory(
+ $validatorMetadataFactory->reveal(),
+ $decoratedPropertyMetadataFactory->reveal(),
+ [new PropertySchemaRangeRestriction()]
+ );
+ $schema = $validationPropertyMetadataFactory->create(DummyRangeValidatedEntity::class, $property)->getSchema();
+
+ $this->assertSame($expectedSchema, $schema);
+ }
+
+ public function provideRangeConstraintCases(): \Generator
+ {
+ yield 'min int' => ['type' => new Type(Type::BUILTIN_TYPE_INT), 'property' => 'dummyIntMin', 'expectedSchema' => ['minimum' => 1]];
+ yield 'max int' => ['type' => new Type(Type::BUILTIN_TYPE_INT), 'property' => 'dummyIntMax', 'expectedSchema' => ['maximum' => 10]];
+ yield 'min/max int' => ['type' => new Type(Type::BUILTIN_TYPE_INT), 'property' => 'dummyIntMinMax', 'expectedSchema' => ['minimum' => 1, 'maximum' => 10]];
+ yield 'min float' => ['type' => new Type(Type::BUILTIN_TYPE_FLOAT), 'property' => 'dummyFloatMin', 'expectedSchema' => ['minimum' => 1.5]];
+ yield 'max float' => ['type' => new Type(Type::BUILTIN_TYPE_FLOAT), 'property' => 'dummyFloatMax', 'expectedSchema' => ['maximum' => 10.5]];
+ yield 'min/max float' => ['type' => new Type(Type::BUILTIN_TYPE_FLOAT), 'property' => 'dummyFloatMinMax', 'expectedSchema' => ['minimum' => 1.5, 'maximum' => 10.5]];
+ }
}
diff --git a/tests/Fixtures/DummyRangeValidatedEntity.php b/tests/Fixtures/DummyRangeValidatedEntity.php
new file mode 100644
index 00000000000..5a2a9a0cc5a
--- /dev/null
+++ b/tests/Fixtures/DummyRangeValidatedEntity.php
@@ -0,0 +1,61 @@
+
+ *
+ * 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\Core\Tests\Fixtures;
+
+use Symfony\Component\Validator\Constraints as Assert;
+
+class DummyRangeValidatedEntity
+{
+ /**
+ * @var int
+ *
+ * @Assert\Range(min=1)
+ */
+ public $dummyIntMin;
+
+ /**
+ * @var int
+ *
+ * @Assert\Range(max=10)
+ */
+ public $dummyIntMax;
+
+ /**
+ * @var int
+ *
+ * @Assert\Range(min=1, max=10)
+ */
+ public $dummyIntMinMax;
+
+ /**
+ * @var float
+ *
+ * @Assert\Range(min=1.5)
+ */
+ public $dummyFloatMin;
+
+ /**
+ * @var float
+ *
+ * @Assert\Range(max=10.5)
+ */
+ public $dummyFloatMax;
+
+ /**
+ * @var float
+ *
+ * @Assert\Range(min=1.5, max=10.5)
+ */
+ public $dummyFloatMinMax;
+}