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; +}