Skip to content

Commit

Permalink
fix: add oneOf property schema restriction
Browse files Browse the repository at this point in the history
  • Loading branch information
alanpoulain committed Mar 19, 2021
1 parent 721e66c commit 0e629fa
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
## 2.6.4

* Serializer: Fix denormalization of basic property-types in XML and CSV (#3191)
* JSON Schema: Manage Sequentially and AtLeastOneOf constraints when generating property metadata (#4139 and #4147)
* Doctrine: Fix purging HTTP cache for unreadable relations (#3441)
* Doctrine: Revert #3774 support for binary UUID in search filter (#4134)
* GraphQL: Partial pagination support (#3223)
Expand Down
5 changes: 5 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/validator.xml
Expand Up @@ -21,6 +21,11 @@
<tag name="api_platform.metadata.property_schema_restriction"/>
</service>

<service id="api_platform.metadata.property_schema.one_of_restriction" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaOneOfRestriction" public="false">
<argument type="tagged" tag="api_platform.metadata.property_schema_restriction" />
<tag name="api_platform.metadata.property_schema_restriction"/>
</service>

<service id="api_platform.metadata.property_schema.regex_restriction" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRegexRestriction" public="false">
<tag name="api_platform.metadata.property_schema_restriction"/>
</service>
Expand Down
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\AtLeastOneOf;

/**
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class PropertySchemaOneOfRestriction implements PropertySchemaRestrictionMetadataInterface
{
/**
* @var iterable<PropertySchemaRestrictionMetadataInterface>
*/
private $restrictionsMetadata;

/**
* @param iterable<PropertySchemaRestrictionMetadataInterface> $restrictionsMetadata
*/
public function __construct(iterable $restrictionsMetadata = [])
{
$this->restrictionsMetadata = $restrictionsMetadata;
}

/**
* {@inheritdoc}
*
* @param AtLeastOneOf $constraint
*/
public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array
{
$oneOfConstraints = $constraint->getNestedContraints();
$oneOfRestrictions = [];

foreach ($oneOfConstraints as $oneOfConstraint) {
foreach ($this->restrictionsMetadata as $restrictionMetadata) {
if ($restrictionMetadata->supports($oneOfConstraint, $propertyMetadata)) {
$oneOfRestrictions[] = $restrictionMetadata->create($oneOfConstraint, $propertyMetadata);
}
}
}

if (!empty($oneOfRestrictions)) {
return ['oneOf' => $oneOfRestrictions];
}

return [];
}

/**
* {@inheritdoc}
*/
public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool
{
if (!($constraint instanceof AtLeastOneOf)) {
return false;
}

foreach ($constraint->getNestedContraints() as $nestedContraint) {
foreach ($this->restrictionsMetadata as $restrictionMetadata) {
if ($restrictionMetadata->supports($nestedContraint, $propertyMetadata)) {
return true;
}
}
}

return false;
}
}
Expand Up @@ -1333,6 +1333,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
'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.one_of_restriction',
'api_platform.metadata.property_schema.regex_restriction',
'api_platform.metadata.property_schema.format_restriction',
'api_platform.metadata.property.metadata_factory.yaml',
Expand Down
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace ApiPlatform\Core\Tests\Bridge\Symfony\Validator\Metadata\Property\Restriction;

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\PropertySchemaRegexRestriction;
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\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Positive;

/**
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class PropertySchemaOneOfRestrictionTest extends TestCase
{
use ProphecyTrait;

private $propertySchemaOneOfRestriction;

protected function setUp(): void
{
$this->propertySchemaOneOfRestriction = new PropertySchemaOneOfRestriction([
new PropertySchemaLengthRestriction(),
new PropertySchemaRegexRestriction(),
]);
}

/**
* @dataProvider supportsProvider
*/
public function testSupports(Constraint $constraint, PropertyMetadata $propertyMetadata, bool $expectedResult): void
{
if (!class_exists(AtLeastOneOf::class)) {
self::markTestSkipped();
}

self::assertSame($expectedResult, $this->propertySchemaOneOfRestriction->supports($constraint, $propertyMetadata));
}

public function supportsProvider(): \Generator
{
yield 'empty' => [new AtLeastOneOf(['constraints' => []]), new PropertyMetadata(), false];

yield 'not supported constraints' => [new AtLeastOneOf(['constraints' => [new Positive(), new Length(['min' => 3])]]), new PropertyMetadata(), false];

yield 'one supported constraint' => [new AtLeastOneOf(['constraints' => [new Positive(), new Length(['min' => 3])]]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)), true];
}

/**
* @dataProvider createProvider
*/
public function testCreate(Constraint $constraint, PropertyMetadata $propertyMetadata, array $expectedResult): void
{
if (!class_exists(AtLeastOneOf::class)) {
self::markTestSkipped();
}

self::assertSame($expectedResult, $this->propertySchemaOneOfRestriction->create($constraint, $propertyMetadata));
}

public function createProvider(): \Generator
{
yield 'empty' => [new AtLeastOneOf(['constraints' => []]), new PropertyMetadata(), []];

yield 'not supported constraints' => [new AtLeastOneOf(['constraints' => [new Positive(), new Length(['min' => 3])]]), new PropertyMetadata(), []];

yield 'one supported constraint' => [new AtLeastOneOf(['constraints' => [new Positive(), new Length(['min' => 3])]]), new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)), [
'oneOf' => [['minLength' => 3]],
]];
}
}
Expand Up @@ -15,17 +15,20 @@

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\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\DummySequentiallyValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyValidatedEntity;
use ApiPlatform\Core\Tests\ProphecyTrait;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\Sequentially;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
Expand Down Expand Up @@ -368,4 +371,39 @@ public function testCreateWithSequentiallyConstraint(): void
$this->assertArrayHasKey('maxLength', $schema);
$this->assertArrayHasKey('pattern', $schema);
}

public function testCreateWithAtLeastOneOfConstraint(): void
{
if (!class_exists(AtLeastOneOf::class)) {
$this->markTestSkipped();
}

$validatorClassMetadata = new ClassMetadata(DummyAtLeastOneOfValidatedEntity::class);
(new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata);

$validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class);
$validatorMetadataFactory->getMetadataFor(DummyAtLeastOneOfValidatedEntity::class)
->willReturn($validatorClassMetadata)
->shouldBeCalled();

$decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
$decoratedPropertyMetadataFactory->create(DummyAtLeastOneOfValidatedEntity::class, 'dummy', [])->willReturn(
new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING))
)->shouldBeCalled();
$restrictionsMetadata = [new PropertySchemaLengthRestriction(), new PropertySchemaRegexRestriction()];
$restrictionsMetadata = [new PropertySchemaOneOfRestriction($restrictionsMetadata), new PropertySchemaLengthRestriction(), new PropertySchemaRegexRestriction()];
$validationPropertyMetadataFactory = new ValidatorPropertyMetadataFactory(
$validatorMetadataFactory->reveal(),
$decoratedPropertyMetadataFactory->reveal(),
$restrictionsMetadata
);
$schema = $validationPropertyMetadataFactory->create(DummyAtLeastOneOfValidatedEntity::class, 'dummy')->getSchema();

$this->assertNotNull($schema);
$this->assertArrayHasKey('oneOf', $schema);
$this->assertSame([
['pattern' => '.*#.*'],
['minLength' => 10],
], $schema['oneOf']);
}
}
29 changes: 29 additions & 0 deletions tests/Fixtures/DummyAtLeastOneOfValidatedEntity.php
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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 DummyAtLeastOneOfValidatedEntity
{
/**
* @var string
*
* @Assert\AtLeastOneOf({
* @Assert\Regex("/#/"),
* @Assert\Length(min=10)
* })
*/
public $dummy;
}

0 comments on commit 0e629fa

Please sign in to comment.