From 38d69556f7b33a2f6cf56e8eb4ffc76290144745 Mon Sep 17 00:00:00 2001 From: penja Date: Sat, 28 Dec 2019 17:36:58 +0200 Subject: [PATCH] add property schema restrictions based on validation constraints --- .../ApiPlatformExtension.php | 3 + .../Bundle/Resources/config/validator.xml | 13 ++ .../Restriction/PropertySchemaFormat.php | 62 +++++++ .../PropertySchemaLengthRestriction.php | 70 ++++++++ .../PropertySchemaRegexRestriction.php | 42 +++++ ...ertySchemaRestrictionMetadataInterface.php | 43 +++++ .../ValidatorPropertyMetadataFactory.php | 81 ++++++--- src/JsonSchema/SchemaFactory.php | 8 +- src/Metadata/Property/PropertyMetadata.php | 23 ++- .../ApiPlatformExtensionTest.php | 8 + .../ValidatorPropertyMetadataFactoryTest.php | 155 +++++++++++++++++- tests/Fixtures/DummyValidatedEntity.php | 30 ++++ .../DocumentationNormalizerV3Test.php | 7 +- 13 files changed, 511 insertions(+), 34 deletions(-) create mode 100644 src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormat.php create mode 100644 src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLengthRestriction.php create mode 100644 src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestriction.php create mode 100644 src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index de9a2031c03..af6d2514150 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -23,6 +23,7 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter as DoctrineOrmAbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\RequestBodySearchCollectionExtensionInterface; +use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; @@ -553,6 +554,8 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr { if (interface_exists(ValidatorInterface::class)) { $loader->load('validator.xml'); + $container->registerForAutoconfiguration(PropertySchemaRestrictionMetadataInterface::class) + ->addTag('api_platform.metadata.property_schema_restriction'); } if (!$config['validator']) { diff --git a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml index 2526c93cfc6..9a5a50a6d7e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml @@ -14,6 +14,19 @@ + + + + + + + + + + + + + diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormat.php b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormat.php new file mode 100644 index 00000000000..67990bfcab5 --- /dev/null +++ b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormat.php @@ -0,0 +1,62 @@ + + * + * 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\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\Ip; +use Symfony\Component\Validator\Constraints\Uuid; + +/** + * Class PropertySchemaFormat. + * + * @author Andrii Penchuk penja7@gmail.com + */ +class PropertySchemaFormat implements PropertySchemaRestrictionMetadataInterface +{ + /** + * {@inheritdoc} + */ + public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array + { + if ($constraint instanceof Email) { + return ['format' => 'email']; + } + + if ($constraint instanceof Uuid) { + return ['format' => 'uuid']; + } + + if ($constraint instanceof Ip) { + if ($constraint->version === $constraint::V4) { + return ['format' => 'ipv4']; + } + + return ['format' => 'ipv6']; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool + { + $schema = $propertyMetadata->getSchema(); + + return empty($schema['format']); + } +} diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLengthRestriction.php b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLengthRestriction.php new file mode 100644 index 00000000000..c6c2a7abffa --- /dev/null +++ b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLengthRestriction.php @@ -0,0 +1,70 @@ + + * + * 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\Length; + +/** + * Class PropertySchemaLengthRestrictions. + * + * @author Andrii Penchuk penja7@gmail.com + */ +class PropertySchemaLengthRestriction implements PropertySchemaRestrictionMetadataInterface +{ + /** + * {@inheritdoc} + */ + public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array + { + $restriction = []; + + switch ($propertyMetadata->getType()->getBuiltinType()) { + case Type::BUILTIN_TYPE_STRING: + + if (isset($constraint->min)) { + $restriction['minLength'] = (int) $constraint->min; + } + + if (isset($constraint->max)) { + $restriction['maxLength'] = (int) $constraint->max; + } + + break; + case Type::BUILTIN_TYPE_INT: + case Type::BUILTIN_TYPE_FLOAT: + if (isset($constraint->min)) { + $restriction['minimum'] = (int) $constraint->min; + } + + if (isset($constraint->max)) { + $restriction['maximum'] = (int) $constraint->max; + } + + break; + } + + return $restriction; + } + + /** + * {@inheritdoc} + */ + public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool + { + return $constraint instanceof Length && null !== $propertyMetadata->getType(); + } +} diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestriction.php b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestriction.php new file mode 100644 index 00000000000..dccc81e9fbd --- /dev/null +++ b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestriction.php @@ -0,0 +1,42 @@ + + * + * 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\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Regex; + +/** + * Class PropertySchemaRegexRestriction. + * + * @author Andrii Penchuk penja7@gmail.com + */ +class PropertySchemaRegexRestriction implements PropertySchemaRestrictionMetadataInterface +{ + /** + * {@inheritdoc} + */ + public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array + { + return isset($constraint->pattern) ? ['pattern' => $constraint->pattern] : []; + } + + /** + * {@inheritdoc} + */ + public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool + { + return $constraint instanceof Regex && $constraint->match; + } +} diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php new file mode 100644 index 00000000000..2486342de40 --- /dev/null +++ b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php @@ -0,0 +1,43 @@ + + * + * 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\Validator\Constraint; + +/** + * Interface PropertySchemaRestrictionsInterface. + * + * @author Andrii Penchuk penja7@gmail.com + */ +interface PropertySchemaRestrictionMetadataInterface +{ + /** + * Creates json schema restrictions based on the validation constraints. + * + * @param Constraint $constraint The validation constraint + * @param PropertyMetadata $propertyMetadata The property metadata + * + * @return array The array of restrictions + */ + public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array; + + /** + * Is the constraint supported by the schema restriction? + * + * @param Constraint $constraint The validation constraint + * @param PropertyMetadata $propertyMetadata The property metadata + */ + public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool; +} diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php b/src/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php index 5568720c592..438f20d1a2d 100644 --- a/src/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php +++ b/src/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property; +use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use Symfony\Component\Validator\Constraint; @@ -67,11 +68,19 @@ final class ValidatorPropertyMetadataFactory implements PropertyMetadataFactoryI private $decorated; private $validatorMetadataFactory; + /** + * @var iterable + */ + private $restrictionsMetadata; - public function __construct(ValidatorMetadataFactoryInterface $validatorMetadataFactory, PropertyMetadataFactoryInterface $decorated) + /** + * @param PropertySchemaRestrictionMetadataInterface[] $restrictionsMetadata + */ + public function __construct(ValidatorMetadataFactoryInterface $validatorMetadataFactory, PropertyMetadataFactoryInterface $decorated, iterable $restrictionsMetadata = []) { $this->validatorMetadataFactory = $validatorMetadataFactory; $this->decorated = $decorated; + $this->restrictionsMetadata = $restrictionsMetadata; } /** @@ -83,26 +92,23 @@ public function create(string $resourceClass, string $name, array $options = []) $required = $propertyMetadata->isRequired(); $iri = $propertyMetadata->getIri(); + $schema = $propertyMetadata->getSchema(); - if (null !== $required && null !== $iri) { + if (null !== $required && null !== $iri && null !== $schema) { return $propertyMetadata; } $validatorClassMetadata = $this->validatorMetadataFactory->getMetadataFor($resourceClass); + if (!$validatorClassMetadata instanceof ValidatorClassMetadataInterface) { throw new \UnexpectedValueException(sprintf('Validator class metadata expected to be of type "%s".', ValidatorClassMetadataInterface::class)); } - foreach ($validatorClassMetadata->getPropertyMetadata($name) as $validatorPropertyMetadata) { - if (null === $required && isset($options['validation_groups'])) { - $required = $this->isRequiredByGroups($validatorPropertyMetadata, $options); - } - - if (!method_exists($validatorClassMetadata, 'getDefaultGroup')) { - throw new \UnexpectedValueException(sprintf('Validator class metadata expected to have method "%s".', 'getDefaultGroup')); - } + $validationGroups = $this->getValidationGroups($validatorClassMetadata, $options); + $restrictions = []; - foreach ($validatorPropertyMetadata->findConstraints($validatorClassMetadata->getDefaultGroup()) as $constraint) { + foreach ($validatorClassMetadata->getPropertyMetadata($name) as $validatorPropertyMetadata) { + foreach ($this->getPropertyConstraints($validatorPropertyMetadata, $validationGroups) as $constraint) { if (null === $required && $this->isRequired($constraint)) { $required = true; } @@ -111,33 +117,64 @@ public function create(string $resourceClass, string $name, array $options = []) $iri = self::SCHEMA_MAPPED_CONSTRAINTS[\get_class($constraint)] ?? null; } - if (null !== $required && null !== $iri) { - break 2; + foreach ($this->restrictionsMetadata as $restrictionMetadata) { + if ($restrictionMetadata->supports($constraint, $propertyMetadata)) { + $restrictions[] = $restrictionMetadata->create($constraint, $propertyMetadata); + } } } } - return $propertyMetadata->withIri($iri)->withRequired($required ?? false); + $propertyMetadata = $propertyMetadata->withIri($iri)->withRequired($required ?? false); + + if (!empty($restrictions)) { + if (null === $schema) { + $schema = []; + } + + $schema += array_merge(...$restrictions); + $propertyMetadata = $propertyMetadata->withSchema($schema); + } + + return $propertyMetadata; } /** - * Tests if the property is required because of its validation groups. + * Returns the list of validation groups. */ - private function isRequiredByGroups(ValidatorPropertyMetadataInterface $validatorPropertyMetadata, array $options): bool + private function getValidationGroups(ValidatorClassMetadataInterface $classMetadata, array $options): array { - foreach ($options['validation_groups'] as $validationGroup) { + if (isset($options['validation_groups'])) { + return $options['validation_groups']; + } + + if (!method_exists($classMetadata, 'getDefaultGroup')) { + throw new \UnexpectedValueException(sprintf('Validator class metadata expected to have method "%s".', 'getDefaultGroup')); + } + + return [$classMetadata->getDefaultGroup()]; + } + + /** + * Tests if the property is required because of its validation groups. + */ + private function getPropertyConstraints( + ValidatorPropertyMetadataInterface $validatorPropertyMetadata, + array $groups + ): array { + $constraints = []; + + foreach ($groups as $validationGroup) { if (!\is_string($validationGroup)) { continue; } - foreach ($validatorPropertyMetadata->findConstraints($validationGroup) as $constraint) { - if ($this->isRequired($constraint)) { - return true; - } + foreach ($validatorPropertyMetadata->findConstraints($validationGroup) as $propertyConstraint) { + $constraints[] = $propertyConstraint; } } - return false; + return $constraints; } /** diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index fe673a83bfc..8790b6af5da 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -153,6 +153,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str { $version = $schema->getVersion(); $swagger = false; + $propertySchema = $propertyMetadata->getSchema() ?? []; + switch ($version) { case Schema::VERSION_SWAGGER: $swagger = true; @@ -165,7 +167,11 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $basePropertySchemaAttribute = 'json_schema_context'; } - $propertySchema = $propertyMetadata->getAttributes()[$basePropertySchemaAttribute] ?? []; + $propertySchema = array_merge( + $propertySchema, + $propertyMetadata->getAttributes()[$basePropertySchemaAttribute] ?? [] + ); + if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { $propertySchema['readOnly'] = true; } diff --git a/src/Metadata/Property/PropertyMetadata.php b/src/Metadata/Property/PropertyMetadata.php index 1018d9726ec..4629b89e291 100644 --- a/src/Metadata/Property/PropertyMetadata.php +++ b/src/Metadata/Property/PropertyMetadata.php @@ -46,8 +46,9 @@ final class PropertyMetadata * @var null */ private $example; + private $schema; - public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null, bool $initializable = null, $default = null, $example = null) + public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null, bool $initializable = null, $default = null, $example = null, array $schema = null) { $this->type = $type; $this->description = $description; @@ -67,6 +68,7 @@ public function __construct(Type $type = null, string $description = null, bool $this->initializable = $initializable; $this->default = $default; $this->example = $example; + $this->schema = $schema; } /** @@ -397,4 +399,23 @@ public function withExample($example): self return $metadata; } + + /** + * @return array + */ + public function getSchema(): ?array + { + return $this->schema; + } + + /** + * Returns a new instance with the given schema. + */ + public function withSchema(array $schema = null): self + { + $metadata = clone $this; + $metadata->schema = $schema; + + return $metadata; + } } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 604c1d508eb..a9dc1fdb20c 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -55,6 +55,7 @@ use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\TermFilter; use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\ApiPlatformExtension; +use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface; use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; @@ -1089,6 +1090,10 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); $this->childDefinitionProphecy->setBindings(['$requestStack' => null])->shouldBeCalledTimes(1); + $containerBuilderProphecy->registerForAutoconfiguration(PropertySchemaRestrictionMetadataInterface::class) + ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); + $this->childDefinitionProphecy->addTag('api_platform.metadata.property_schema_restriction')->shouldBeCalledTimes(1); + if (\in_array('odm', $doctrineIntegrationsToLoad, true)) { $containerBuilderProphecy->registerForAutoconfiguration(AggregationItemExtensionInterface::class) ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); @@ -1231,6 +1236,9 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.metadata.extractor.yaml', 'api_platform.metadata.property.metadata_factory.annotation', 'api_platform.metadata.property.metadata_factory.validator', + 'api_platform.metadata.property_schema.length_restriction', + 'api_platform.metadata.property_schema.regex_restriction', + 'api_platform.metadata.property_schema.format_restriction', 'api_platform.metadata.property.metadata_factory.yaml', 'api_platform.metadata.property.name_collection_factory.yaml', 'api_platform.metadata.resource.filter_metadata_factory.annotation', diff --git a/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php b/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php index 3ae3a29a27e..1e989499ecd 100644 --- a/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php +++ b/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php @@ -13,6 +13,9 @@ namespace ApiPlatform\Core\Tests\Bridge\Symfony\Validator\Metadata\Property; +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\PropertySchemaRegexRestriction; use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -20,6 +23,7 @@ use ApiPlatform\Core\Tests\Fixtures\DummyValidatedEntity; use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; @@ -48,7 +52,11 @@ public function testCreateWithPropertyWithRequiredConstraints() $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummy'); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -66,7 +74,11 @@ public function testCreateWithPropertyWithNotRequiredConstraints() $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyDate'); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -83,7 +95,11 @@ public function testCreateWithPropertyWithoutConstraints() $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyId'); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -100,7 +116,11 @@ public function testCreateWithPropertyWithRightValidationGroupsAndRequiredConstr $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyGroup', ['validation_groups' => ['dummy']]); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -117,7 +137,11 @@ public function testCreateWithPropertyWithBadValidationGroupsAndRequiredConstrai $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyGroup', ['validation_groups' => ['ymmud']]); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -134,7 +158,11 @@ public function testCreateWithPropertyWithNonStringValidationGroupsAndRequiredCo $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyGroup', ['validation_groups' => [1312]]); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -149,8 +177,13 @@ public function testCreateWithRequiredByDecorated() $decoratedPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyDate', [])->willReturn($propertyMetadata)->shouldBeCalled(); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); + $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class)->willReturn($this->validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummyDate'); $this->assertEquals($expectedPropertyMetadata, $resultedPropertyMetadata); @@ -186,11 +219,117 @@ public function testCreateWithPropertyWithValidationConstraints() $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyIriWithValidationEntity::class)->willReturn($validatorClassMetadata)->shouldBeCalled(); - $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory($validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal()); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [] + ); foreach ($types as $property => $iri) { $resultedPropertyMetadata = $validatorPropertyMetadataFactory->create(DummyIriWithValidationEntity::class, $property); $this->assertSame($iri, $resultedPropertyMetadata->getIri()); } } + + public function testCreateWithPropertyLengthRestriction(): void + { + $validatorClassMetadata = new ClassMetadata(DummyValidatedEntity::class); + (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + + $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); + $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class) + ->willReturn($validatorClassMetadata) + ->shouldBeCalled(); + + $decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $property = 'dummy'; + $decoratedPropertyMetadataFactory->create(DummyValidatedEntity::class, $property, [])->willReturn( + new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)) + )->shouldBeCalled(); + + $lengthRestrictions = new PropertySchemaLengthRestriction(); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal(), [$lengthRestrictions] + ); + + $schema = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, $property)->getSchema(); + $this->assertNotNull($schema); + $this->assertArrayHasKey('minLength', $schema); + $this->assertArrayHasKey('maxLength', $schema); + + $numberTypes = [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT]; + + foreach ($numberTypes as $type) { + $decoratedPropertyMetadataFactory->create(DummyValidatedEntity::class, $property, [])->willReturn( + new PropertyMetadata(new Type($type)) + )->shouldBeCalled(); + $validatorPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal(), [$lengthRestrictions] + ); + + $schema = $validatorPropertyMetadataFactory->create(DummyValidatedEntity::class, $property)->getSchema(); + $this->assertNotNull($schema); + $this->assertArrayHasKey('minimum', $schema); + $this->assertArrayHasKey('maximum', $schema); + } + } + + public function testCreateWithPropertyRegexRestriction(): void + { + $validatorClassMetadata = new ClassMetadata(DummyValidatedEntity::class); + (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + + $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); + $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class) + ->willReturn($validatorClassMetadata) + ->shouldBeCalled(); + + $decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $decoratedPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummy', [])->willReturn( + new PropertyMetadata() + )->shouldBeCalled(); + + $validationPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), $decoratedPropertyMetadataFactory->reveal(), + [new PropertySchemaRegexRestriction()] + ); + + $schema = $validationPropertyMetadataFactory->create(DummyValidatedEntity::class, 'dummy')->getSchema(); + $this->assertNotNull($schema); + $this->assertArrayHasKey('pattern', $schema); + $this->assertEquals('^dummy$', $schema['pattern']); + } + + public function testCreateWithPropertyFormatRestriction(): void + { + $validatorClassMetadata = new ClassMetadata(DummyValidatedEntity::class); + (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + + $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); + $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class) + ->willReturn($validatorClassMetadata) + ->shouldBeCalled(); + $formats = [ + 'dummyEmail' => 'email', + 'dummyUuid' => 'uuid', + 'dummyIpv4' => 'ipv4', + 'dummyIpv6' => 'ipv6', + ]; + + foreach ($formats as $property => $format) { + $decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $decoratedPropertyMetadataFactory->create(DummyValidatedEntity::class, $property, [])->willReturn( + new PropertyMetadata() + )->shouldBeCalled(); + $validationPropertyMetadataFactory = new ValidatorPropertyMetadataFactory( + $validatorMetadataFactory->reveal(), + $decoratedPropertyMetadataFactory->reveal(), + [new PropertySchemaFormat()] + ); + $schema = $validationPropertyMetadataFactory->create(DummyValidatedEntity::class, $property)->getSchema(); + $this->assertNotNull($schema); + $this->assertArrayHasKey('format', $schema); + $this->assertEquals($format, $schema['format']); + } + } } diff --git a/tests/Fixtures/DummyValidatedEntity.php b/tests/Fixtures/DummyValidatedEntity.php index 8314774b654..81d3e4b852d 100644 --- a/tests/Fixtures/DummyValidatedEntity.php +++ b/tests/Fixtures/DummyValidatedEntity.php @@ -31,9 +31,39 @@ class DummyValidatedEntity * @var string A dummy * * @Assert\NotBlank + * @Assert\Length(max="4", min="10") + * @Assert\Regex(pattern="^dummy$") */ public $dummy; + /** + * @var string + * + * @Assert\Email + */ + public $dummyEmail; + + /** + * @var string + * + * @Assert\Uuid + */ + public $dummyUuid; + + /** + * @var string + * + * @Assert\Ip + */ + public $dummyIpv4; + + /** + * @var string + * + * @Assert\Ip(version="6") + */ + public $dummyIpv6; + /** * @var \DateTimeInterface A dummy date * diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php index d6250fb3b90..92499eafe50 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -111,8 +111,8 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [])); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, null, null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null)); $propertyMetadataFactoryProphecy->create(Dummy::class, 'description')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is an initializable but not writable property.', true, false, true, true, false, false, null, null, [], null, true)); $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class), 'This is a \DateTimeInterface object.', true, true, true, true, false, false, null, null, [])); @@ -375,6 +375,9 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'type' => 'integer', 'description' => 'This is an id.', 'readOnly' => true, + 'minLength' => 3, + 'maxLength' => 20, + 'pattern' => '^dummyPattern$', ]), 'name' => new \ArrayObject([ 'type' => 'string',