Skip to content

Commit

Permalink
Merge 9948a54 into 69229fb
Browse files Browse the repository at this point in the history
  • Loading branch information
norkunas committed Apr 1, 2021
2 parents 69229fb + 9948a54 commit 6bc8862
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

## 2.7.0

* JSON Schema: Add support for generating property schema with Collection restriction (#4182)
* JSON Schema: Manage Compound constraint when generating property metadata (#4180)
* Validator: Add an option to disable query parameter validation (#4165)
* JSON Schema: Add support for generating property schema with Choice restriction (#4162)
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.collection_restriction" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaCollectionRestriction" 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.length_restriction" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaLengthRestriction" public="false">
<tag name="api_platform.metadata.property_schema_restriction"/>
</service>
Expand Down
@@ -0,0 +1,128 @@
<?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\Collection;
use Symfony\Component\Validator\Constraints\Optional;
use Symfony\Component\Validator\Constraints\Required;
use Symfony\Component\Validator\Constraints\Type;

/**
* @author Tomas Norkūnas <norkunas.tom@gmail.com>
*/
final class PropertySchemaCollectionRestriction implements PropertySchemaRestrictionMetadataInterface
{
/**
* @var iterable<PropertySchemaRestrictionMetadataInterface>
*/
private $restrictionsMetadata;

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

/**
* {@inheritdoc}
*
* @param Collection $constraint
*/
public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array
{
$restriction = [
'type' => 'object',
'properties' => [],
'additionalProperties' => $constraint->allowExtraFields,
];
$required = [];

foreach ($constraint->fields as $field => $baseConstraint) {
/** @var Required|Optional $baseConstraint */
if ($baseConstraint instanceof Required) {
$required[] = $field;
}

$restriction['properties'][$field] = $this->mergeConstraintRestrictions($baseConstraint, $propertyMetadata);
}

if ($required) {
$restriction['required'] = $required;
}

return $restriction;
}

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

/**
* @param Required|Optional $constraint
*/
private function mergeConstraintRestrictions(Constraint $constraint, PropertyMetadata $propertyMetadata): array
{
$propertyRestrictions = [['type' => 'string']];
$nestedConstraints = method_exists($constraint, 'getNestedContraints') ? $constraint->getNestedContraints() : $constraint->constraints;

foreach ($nestedConstraints as $nestedContraint) {
if ($nestedContraint instanceof Type) {
/** @var string $nestedType */
$nestedType = $nestedContraint->type;
$propertyRestrictions[0]['type'] = $this->getConstraintType($nestedType);
}

foreach ($this->restrictionsMetadata as $restrictionMetadata) {
if ($restrictionMetadata->supports($nestedContraint, $propertyMetadata) && !empty($nestedContraintRestriction = $restrictionMetadata->create($nestedContraint, $propertyMetadata))) {
$propertyRestrictions[] = $nestedContraintRestriction;
}
}
}

return array_merge([], ...$propertyRestrictions);
}

private function getConstraintType(string $type): string
{
if (\in_array($type, ['bool', 'boolean'], true)) {
return 'boolean';
}

if (\in_array($type, ['int', 'integer', 'long'], true)) {
return 'integer';
}

if (\in_array($type, ['float', 'double', 'real', 'numeric'], true)) {
return 'number';
}

if (\in_array($type, ['array', 'iterable'], true)) {
return 'array';
}

if ('object' === $type) {
return 'object';
}

return 'string';
}
}
Expand Up @@ -1337,6 +1337,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
'api_platform.metadata.property.metadata_factory.annotation',
'api_platform.metadata.property.metadata_factory.validator',
'api_platform.metadata.property_schema.choice_restriction',
'api_platform.metadata.property_schema.collection_restriction',
'api_platform.metadata.property_schema.length_restriction',
'api_platform.metadata.property_schema.one_of_restriction',
'api_platform.metadata.property_schema.range_restriction',
Expand Down
@@ -0,0 +1,121 @@
<?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\Bridge\Symfony\Validator\Metadata\Property\Restriction;

use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaCollectionRestriction;
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\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Tests\ProphecyTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Optional;
use Symfony\Component\Validator\Constraints\Positive;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\Required;

/**
* @author Tomas Norkūnas <norkunas.tom@gmail.com>
*/
final class PropertySchemaCollectionRestrictionTest extends TestCase
{
use ProphecyTrait;

private $propertySchemaCollectionRestriction;

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

/**
* @dataProvider supportsProvider
*/
public function testSupports(Constraint $constraint, PropertyMetadata $propertyMetadata, bool $expectedResult): void
{
self::assertSame($expectedResult, $this->propertySchemaCollectionRestriction->supports($constraint, $propertyMetadata));
}

public function supportsProvider(): \Generator
{
yield 'supported' => [new Collection(['fields' => []]), new PropertyMetadata(), true];

yield 'not supported' => [new Positive(), new PropertyMetadata(), false];
}

/**
* @dataProvider createProvider
*/
public function testCreate(Constraint $constraint, PropertyMetadata $propertyMetadata, array $expectedResult): void
{
self::assertSame($expectedResult, $this->propertySchemaCollectionRestriction->create($constraint, $propertyMetadata));
}

public function createProvider(): \Generator
{
yield 'empty' => [new Collection(['fields' => []]), new PropertyMetadata(), ['type' => 'object', 'properties' => [], 'additionalProperties' => false]];

yield 'with fields' => [
new Collection([
'allowExtraFields' => true,
'fields' => [
'name' => new Required([
new NotBlank(),
]),
'email' => [
new NotNull(),
new Length(['min' => 2, 'max' => 255]),
new Email(['mode' => Email::VALIDATION_MODE_LOOSE]),
],
'phone' => new Optional([
new \Symfony\Component\Validator\Constraints\Type(['type' => 'string']),
new Regex(['pattern' => '/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\.\/0-9]*$/']),
]),
'age' => new Optional([
new \Symfony\Component\Validator\Constraints\Type(['type' => 'int']),
]),
'social' => new Collection([
'fields' => [
'githubUsername' => new NotNull(),
],
]),
],
]),
new PropertyMetadata(),
[
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'email' => ['type' => 'string', 'format' => 'email'],
'phone' => ['type' => 'string', 'pattern' => '[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*'],
'age' => ['type' => 'integer'],
'social' => ['type' => 'object', 'properties' => ['githubUsername' => ['type' => 'string']], 'additionalProperties' => false, 'required' => ['githubUsername']],
],
'additionalProperties' => true,
'required' => ['name', 'email', 'social'],
],
];
}
}
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Validator\Metadata\Property;

use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaChoiceRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaCollectionRestriction;
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;
Expand All @@ -24,6 +25,7 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Tests\Fixtures\DummyAtLeastOneOfValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyCollectionValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyCompoundValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyIriWithValidationEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyRangeValidatedEntity;
Expand Down Expand Up @@ -547,4 +549,67 @@ public function provideChoiceConstraintCases(): \Generator
yield 'multi choice max' => ['propertyMetadata' => new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)), 'property' => 'dummyMultiChoiceMax', 'expectedSchema' => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ['a', 'b', 'c', 'd']], 'maxItems' => 4]];
yield 'multi choice min/max' => ['propertyMetadata' => new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)), 'property' => 'dummyMultiChoiceMinMax', 'expectedSchema' => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ['a', 'b', 'c', 'd']], 'minItems' => 2, 'maxItems' => 4]];
}

public function testCreateWithPropertyCollectionRestriction(): void
{
$validatorClassMetadata = new ClassMetadata(DummyCollectionValidatedEntity::class);
(new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata);

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

$decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
$decoratedPropertyMetadataFactory->create(DummyCollectionValidatedEntity::class, 'dummyData', [])->willReturn(
new PropertyMetadata(new Type(Type::BUILTIN_TYPE_ARRAY))
)->shouldBeCalled();

$lengthRestriction = new PropertySchemaLengthRestriction();
$regexRestriction = new PropertySchemaRegexRestriction();
$formatRestriction = new PropertySchemaFormat();
$restrictionsMetadata = [
$lengthRestriction,
$regexRestriction,
$formatRestriction,
new PropertySchemaCollectionRestriction([
$lengthRestriction,
$regexRestriction,
$formatRestriction,
new PropertySchemaCollectionRestriction([
$lengthRestriction,
$regexRestriction,
$formatRestriction,
]),
]),
];

$validationPropertyMetadataFactory = new ValidatorPropertyMetadataFactory(
$validatorMetadataFactory->reveal(),
$decoratedPropertyMetadataFactory->reveal(),
$restrictionsMetadata
);

$schema = $validationPropertyMetadataFactory->create(DummyCollectionValidatedEntity::class, 'dummyData')->getSchema();

$this->assertSame([
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'email' => ['type' => 'string', 'format' => 'email'],
'phone' => ['type' => 'string', 'pattern' => '[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*'],
'age' => ['type' => 'integer'],
'social' => [
'type' => 'object',
'properties' => [
'githubUsername' => ['type' => 'string'],
],
'additionalProperties' => false,
'required' => ['githubUsername'],
],
],
'additionalProperties' => true,
'required' => ['name', 'email', 'social'],
], $schema);
}
}

0 comments on commit 6bc8862

Please sign in to comment.