Skip to content

Commit

Permalink
Apply enum from Choice Constraints to Items When Choice is Multiple (
Browse files Browse the repository at this point in the history
…#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 <[email protected]>
  • Loading branch information
chrisguitarguy and GuilhemN authored Feb 19, 2021
1 parent ceda09f commit 883d7b6
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 2 deletions.
23 changes: 21 additions & 2 deletions ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;

use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\Validator\Constraints as Assert;

Expand Down Expand Up @@ -81,8 +82,7 @@ public function updateProperty($reflection, OA\Property $property): void
$property->minItems = (int) $annotation->min;
$property->maxItems = (int) $annotation->max;
} elseif ($annotation instanceof Assert\Choice) {
$values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflection->class, $annotation->callback]) : $annotation->choices;
$property->enum = array_values($values);
$this->applyEnumFromChoiceConstraint($property, $annotation, $reflection);
} elseif ($annotation instanceof Assert\Range) {
$property->minimum = (int) $annotation->min;
$property->maximum = (int) $annotation->max;
Expand Down Expand Up @@ -131,4 +131,23 @@ private function appendPattern(OA\Schema $property, $newPattern): void
$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);
}
}
12 changes: 12 additions & 0 deletions Tests/Functional/Entity/SymfonyConstraints.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ class SymfonyConstraints
*/
private $propertyChoiceWithCallbackWithoutClass;

/**
* @var string[]
*
* @Assert\Choice(multiple=true, choices={"choice1", "choice2"})
*/
private $propertyChoiceWithMultiple;

/**
* @var int
*
Expand Down Expand Up @@ -145,6 +152,11 @@ public function setPropertyChoiceWithCallbackWithoutClass(int $propertyChoiceWit
$this->propertyChoiceWithCallbackWithoutClass = $propertyChoiceWithCallbackWithoutClass;
}

public function setPropertyChoiceWithMultiple(array $propertyChoiceWithMultiple): void
{
$this->propertyChoiceWithMultiple = $propertyChoiceWithMultiple;
}

public function setPropertyExpression(int $propertyExpression): void
{
$this->propertyExpression = $propertyExpression;
Expand Down
7 changes: 7 additions & 0 deletions Tests/Functional/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ public function testSymfonyConstraintDocumentation()
'type' => 'integer',
'enum' => ['choice1', 'choice2'],
],
'propertyChoiceWithMultiple' => [
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => ['choice1', 'choice2'],
],
],
'propertyExpression' => [
'type' => 'integer',
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@ public function testAssertChoiceResultsInNumericArray()
$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
*/
Expand Down

0 comments on commit 883d7b6

Please sign in to comment.