Apply enum from Choice Constraints to Items When Choice is Multiple (#1784)

* Apply `enum` from Choice Constraints to Items When Choice is Multiple

Otherwise JSON schema like this is generated:

```
"property": {
  "type": "array",
  "enum": ["one", "two", "three"],
  "items": {
    "type": "string"
  }
}
```

With this change, however, this schema is generated:

```
"property": {
  "type": "array",
  "items": {
    "type": "string",
    "enum": ["one", "two", "three"]
  }
}
```

A possible downside here is that the symfony constraint stuff happens
before types are figured out from PHPDoc. So it's _possible_ to end up
with something that won't validated. Take something like this:

```
/**
 * @Assert\Choice(multiple=true, choices={"..."})
 * @var string
 */
```

This would generate:

```
"property": {
  "type": "string",
  "items": {
    "enum": ["..."]
  }
}
```

* Fix CS

* cs

* more cs

* fix tests

Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
This commit is contained in:
Christopher Davis 2021-02-19 02:41:32 -06:00 committed by GitHub
parent ceda09fe85
commit 883d7b6c89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 2 deletions

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -81,8 +82,7 @@ class SymfonyConstraintAnnotationReader
$property->minItems = (int) $annotation->min; $property->minItems = (int) $annotation->min;
$property->maxItems = (int) $annotation->max; $property->maxItems = (int) $annotation->max;
} elseif ($annotation instanceof Assert\Choice) { } elseif ($annotation instanceof Assert\Choice) {
$values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflection->class, $annotation->callback]) : $annotation->choices; $this->applyEnumFromChoiceConstraint($property, $annotation, $reflection);
$property->enum = array_values($values);
} elseif ($annotation instanceof Assert\Range) { } elseif ($annotation instanceof Assert\Range) {
$property->minimum = (int) $annotation->min; $property->minimum = (int) $annotation->min;
$property->maximum = (int) $annotation->max; $property->maximum = (int) $annotation->max;
@ -131,4 +131,23 @@ class SymfonyConstraintAnnotationReader
$property->pattern = $newPattern; $property->pattern = $newPattern;
} }
} }
/**
* @var ReflectionProperty|ReflectionClass
*/
private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, $reflection): void
{
if ($choice->callback) {
$enumValues = call_user_func(is_array($choice->callback) ? $choice->callback : [$reflection->class, $choice->callback]);
} else {
$enumValues = $choice->choices;
}
$setEnumOnThis = $property;
if ($choice->multiple) {
$setEnumOnThis = Util::getChild($property, OA\Items::class);
}
$setEnumOnThis->enum = array_values($enumValues);
}
} }

View File

@ -71,6 +71,13 @@ class SymfonyConstraints
*/ */
private $propertyChoiceWithCallbackWithoutClass; private $propertyChoiceWithCallbackWithoutClass;
/**
* @var string[]
*
* @Assert\Choice(multiple=true, choices={"choice1", "choice2"})
*/
private $propertyChoiceWithMultiple;
/** /**
* @var int * @var int
* *
@ -145,6 +152,11 @@ class SymfonyConstraints
$this->propertyChoiceWithCallbackWithoutClass = $propertyChoiceWithCallbackWithoutClass; $this->propertyChoiceWithCallbackWithoutClass = $propertyChoiceWithCallbackWithoutClass;
} }
public function setPropertyChoiceWithMultiple(array $propertyChoiceWithMultiple): void
{
$this->propertyChoiceWithMultiple = $propertyChoiceWithMultiple;
}
public function setPropertyExpression(int $propertyExpression): void public function setPropertyExpression(int $propertyExpression): void
{ {
$this->propertyExpression = $propertyExpression; $this->propertyExpression = $propertyExpression;

View File

@ -395,6 +395,13 @@ class FunctionalTest extends WebTestCase
'type' => 'integer', 'type' => 'integer',
'enum' => ['choice1', 'choice2'], 'enum' => ['choice1', 'choice2'],
], ],
'propertyChoiceWithMultiple' => [
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => ['choice1', 'choice2'],
],
],
'propertyExpression' => [ 'propertyExpression' => [
'type' => 'integer', 'type' => 'integer',
], ],

View File

@ -106,6 +106,27 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
$this->assertEquals($schema->properties[0]->enum, ['active', 'blocked']); $this->assertEquals($schema->properties[0]->enum, ['active', 'blocked']);
} }
public function testMultipleChoiceConstraintsApplyEnumToItems()
{
$entity = new class() {
/**
* @Assert\Choice(choices={"one", "two"}, multiple=true)
*/
private $property1;
};
$schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]);
$symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader());
$symfonyConstraintAnnotationReader->setSchema($schema);
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$this->assertInstanceOf(OA\Items::class, $schema->properties[0]->items);
$this->assertEquals($schema->properties[0]->items->enum, ['one', 'two']);
}
/** /**
* @group https://github.com/nelmio/NelmioApiDocBundle/issues/1780 * @group https://github.com/nelmio/NelmioApiDocBundle/issues/1780
*/ */