Add support for compound properties (#1651)

* Add support for compound properties

* Fix CS & Tests

* Another fixing :D

* Final CS fix

* Allow complex compound properties

* cs

* Update the Upgrading guide

* Update php doc

* Add Support for Nullable properties

* Fix CS

* Fix CS

* Add Support for Nullable Types & Schemas as in OA3

* Update Nullable Property handling

* CS

* Fix tests

* Accept also nullable config for Alternative model names

* Refactor nullable refs

* Fix CS & Tests

* Another CS

* Revert "Another CS"

This reverts commit 03ada32b3263f3537d2af63f0abe79bd4a9ac0b5.

* Revert "Fix CS & Tests"

This reverts commit 369f2ccd170aebeeb9d87e9e00cba5cea62d5529.

* Revert "Refactor nullable refs"

This reverts commit 91cdf6fd0130f3ebf415de99f8a91edbc764255e.

* Revert "Revert "Refactor nullable refs""

This reverts commit 0e50fc1938ce3e620fc655a7d1e9284a9f8c24f0.

* Revert "Revert "Fix CS & Tests""

This reverts commit 228d3ca994eb4622c4db81aaa5f32845862e5616.

* Revert "Revert "Another CS""

This reverts commit a5b08dedf5bca8fb711b816c62bed2de9f1c9521.

* Improve nullable refs description

Co-authored-by: Filip Benčo <filip.benco@websupport.sk>
Co-authored-by: Guilhem Niot <guilhem.niot@gmail.com>
This commit is contained in:
Filip Benčo 2020-06-16 13:11:53 +02:00 committed by GitHub
parent d558560f61
commit d932b06bbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 211 additions and 43 deletions

View File

@ -40,7 +40,6 @@ final class ModelRegistry
{
$this->modelDescribers = $modelDescribers;
$this->api = $api;
$this->alternativeNames = []; // last rule wins
foreach (array_reverse($alternativeNames) as $alternativeName => $criteria) {
$this->alternativeNames[] = $model = new Model(new Type('object', false, $criteria['type']), $criteria['groups']);

View File

@ -43,7 +43,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
public function __construct(
PropertyInfoExtractorInterface $propertyInfo,
Reader $reader,
$propertyDescribers,
iterable $propertyDescribers,
array $mediaTypes,
NameConverterInterface $nameConverter = null
) {
@ -99,31 +99,30 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
$types = $this->propertyInfo->getTypes($class, $propertyName);
if (null === $types || 0 === count($types)) {
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@SWG\Property(type="")` to make its type explicit.', $class, $propertyName));
}
if (count($types) > 1) {
throw new \LogicException(sprintf('Property %s::$%s defines more than one type. You can specify the one that should be documented using `@SWG\Property(type="")`.', $class, $propertyName));
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@OA\Property(type="")` to make its type explicit.', $class, $propertyName));
}
$type = $types[0];
$this->describeProperty($type, $model, $property, $propertyName);
$this->describeProperty($types, $model, $property, $propertyName);
}
}
private function describeProperty(Type $type, Model $model, OA\Schema $property, string $propertyName)
/**
* @param Type[] $types
*/
private function describeProperty(array $types, Model $model, OA\Schema $property, string $propertyName)
{
foreach ($this->propertyDescribers as $propertyDescriber) {
if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
$propertyDescriber->setModelRegistry($this->modelRegistry);
}
if ($propertyDescriber->supports($type)) {
$propertyDescriber->describe($type, $property, $model->getGroups());
if ($propertyDescriber->supports($types)) {
$propertyDescriber->describe($types, $property, $model->getGroups());
return;
}
}
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $type->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $types[0]->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
}
public function supports(Model $model): bool

View File

@ -15,7 +15,6 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;
class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
{
@ -29,11 +28,11 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
$this->propertyDescribers = $propertyDescribers;
}
public function describe(Type $type, OA\Schema $property, array $groups = null)
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$type = $type->getCollectionValueType();
$type = $types[0]->getCollectionValueType();
if (null === $type) {
throw new \LogicException(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@SWG\Property(type="array", @SWG\Items(type="string"))`.', $property->title));
throw new \LogicException(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@OA\Property(type="array", @OA\Items(type="string"))`.', $property->title));
}
$property->type = 'array';
@ -43,16 +42,16 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
$propertyDescriber->setModelRegistry($this->modelRegistry);
}
if ($propertyDescriber->supports($type)) {
$propertyDescriber->describe($type, $property, $groups);
if ($propertyDescriber->supports([$type])) {
$propertyDescriber->describe([$type], $property, $groups);
break;
}
}
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return $type->isCollection();
return 1 === count($types) && $types[0]->isCollection();
}
}

View File

@ -16,13 +16,16 @@ use Symfony\Component\PropertyInfo\Type;
class BooleanPropertyDescriber implements PropertyDescriberInterface
{
public function describe(Type $type, OA\Schema $property, array $groups = null)
use NullablePropertyTrait;
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$property->type = 'boolean';
$this->setNullableProperty($types[0], $property);
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return Type::BUILTIN_TYPE_BOOL === $type->getBuiltinType();
return 1 === count($types) && Type::BUILTIN_TYPE_BOOL === $types[0]->getBuiltinType();
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\PropertyDescriber;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
class CompoundPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
/** @var PropertyDescriberInterface[] */
private $propertyDescribers;
public function __construct(iterable $propertyDescribers)
{
$this->propertyDescribers = $propertyDescribers;
}
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$property->oneOf = OA\UNDEFINED !== $property->oneOf ? $property->oneOf : [];
foreach ($types as $type) {
$property->oneOf[] = $schema = Util::createChild($property, OA\Schema::class, []);
foreach ($this->propertyDescribers as $propertyDescriber) {
if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
$propertyDescriber->setModelRegistry($this->modelRegistry);
}
if ($propertyDescriber->supports([$type])) {
$propertyDescriber->describe([$type], $schema, $groups);
break;
}
}
}
}
public function supports(array $types): bool
{
return count($types) >= 2;
}
}

View File

@ -16,15 +16,19 @@ use Symfony\Component\PropertyInfo\Type;
class DateTimePropertyDescriber implements PropertyDescriberInterface
{
public function describe(Type $type, OA\Schema $property, array $groups = null)
use NullablePropertyTrait;
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$property->type = 'string';
$property->format = 'date-time';
$this->setNullableProperty($types[0], $property);
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()
&& is_a($type->getClassName(), \DateTimeInterface::class, true);
return 1 === count($types)
&& Type::BUILTIN_TYPE_OBJECT === $types[0]->getBuiltinType()
&& is_a($types[0]->getClassName(), \DateTimeInterface::class, true);
}
}

View File

@ -16,14 +16,17 @@ use Symfony\Component\PropertyInfo\Type;
class FloatPropertyDescriber implements PropertyDescriberInterface
{
public function describe(Type $type, OA\Schema $property, array $groups = null)
use NullablePropertyTrait;
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$property->type = 'number';
$property->format = 'float';
$this->setNullableProperty($types[0], $property);
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return Type::BUILTIN_TYPE_FLOAT === $type->getBuiltinType();
return 1 === count($types) && Type::BUILTIN_TYPE_FLOAT === $types[0]->getBuiltinType();
}
}

View File

@ -16,13 +16,16 @@ use Symfony\Component\PropertyInfo\Type;
class IntegerPropertyDescriber implements PropertyDescriberInterface
{
public function describe(Type $type, OA\Schema $property, array $groups = null)
use NullablePropertyTrait;
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$property->type = 'integer';
$this->setNullableProperty($types[0], $property);
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return Type::BUILTIN_TYPE_INT === $type->getBuiltinType();
return 1 === count($types) && Type::BUILTIN_TYPE_INT === $types[0]->getBuiltinType();
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\PropertyDescriber;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;
trait NullablePropertyTrait
{
protected function setNullableProperty(Type $type, OA\Schema $property): void
{
if ($type->isNullable()) {
$property->nullable = true;
}
}
}

View File

@ -21,15 +21,22 @@ class ObjectPropertyDescriber implements PropertyDescriberInterface, ModelRegist
{
use ModelRegistryAwareTrait;
public function describe(Type $type, OA\Schema $property, array $groups = null)
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$type = new Type($type->getBuiltinType(), false, $type->getClassName(), $type->isCollection(), $type->getCollectionKeyType(), $type->getCollectionValueType()); // ignore nullable field
$type = new Type($types[0]->getBuiltinType(), false, $types[0]->getClassName(), $types[0]->isCollection(), $types[0]->getCollectionKeyType(), $types[0]->getCollectionValueType()); // ignore nullable field
if ($types[0]->isNullable()) {
$property->nullable = true;
$property->allOf = [['$ref' => $this->modelRegistry->register(new Model($type, $groups))]];
return;
}
$property->ref = $this->modelRegistry->register(new Model($type, $groups));
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType();
return 1 === count($types) && Type::BUILTIN_TYPE_OBJECT === $types[0]->getBuiltinType();
}
}

View File

@ -16,7 +16,13 @@ use Symfony\Component\PropertyInfo\Type;
interface PropertyDescriberInterface
{
public function describe(Type $type, Schema $property, array $groups = null);
/**
* @param Type[] $types
*/
public function describe(array $types, Schema $property, array $groups = null);
public function supports(Type $type): bool;
/**
* @param Type[] $types
*/
public function supports(array $types): bool;
}

View File

@ -16,13 +16,16 @@ use Symfony\Component\PropertyInfo\Type;
class StringPropertyDescriber implements PropertyDescriberInterface
{
public function describe(Type $type, OA\Schema $property, array $groups = null)
use NullablePropertyTrait;
public function describe(array $types, OA\Schema $property, array $groups = null)
{
$property->type = 'string';
$this->setNullableProperty($types[0], $property);
}
public function supports(Type $type): bool
public function supports(array $types): bool
{
return Type::BUILTIN_TYPE_STRING === $type->getBuiltinType();
return 1 === count($types) && Type::BUILTIN_TYPE_STRING === $types[0]->getBuiltinType();
}
}

View File

@ -89,6 +89,12 @@
<tag name="nelmio_api_doc.object_model.property_describer" priority="-1000" />
</service>
<service id="nelmio_api_doc.object_model.property_describers.compound" class="Nelmio\ApiDocBundle\PropertyDescriber\CompoundPropertyDescriber" public="false">
<argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" />
<tag name="nelmio_api_doc.object_model.property_describer" priority="-1001" />
</service>
<!-- Form Type Extensions -->
<service id="nelmio_api_doc.form.documentation_extension" class="Nelmio\ApiDocBundle\Form\Extension\DocumentationExtension">

View File

@ -16,6 +16,7 @@ use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
@ -201,4 +202,13 @@ class ApiController
public function newAreaAction()
{
}
/**
* @Route("/compound", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="Worked well!", @Model(type=CompoundEntity::class))
*/
public function compoundEntityAction()
{
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
class CompoundEntity
{
/**
* @var int|CompoundEntity[]
*/
public $complex;
}

View File

@ -190,7 +190,10 @@ class FunctionalTest extends WebTestCase
'type' => 'array',
],
'friend' => [
'$ref' => '#/components/schemas/User',
'nullable' => true,
'allOf' => [
['$ref' => '#/components/schemas/User'],
],
],
'dummy' => [
'$ref' => '#/components/schemas/Dummy2',
@ -430,4 +433,19 @@ class FunctionalTest extends WebTestCase
$this->assertNotHasProperty('bar', $model);
$this->assertHasProperty('notwhatyouthink', $model);
}
public function testCompoundEntityAction()
{
$model = $this->getModel('CompoundEntity');
$this->assertCount(1, $model->properties);
$this->assertHasProperty('complex', $model);
$property = $model->properties[0];
$this->assertCount(2, $property->oneOf);
$this->assertSame('integer', $property->oneOf[0]->type);
$this->assertSame('array', $property->oneOf[1]->type);
$this->assertSame('#/components/schemas/CompoundEntity', $property->oneOf[1]->items->ref);
}
}

View File

@ -26,3 +26,12 @@ Here are some additional advices that are more likely to apply to NelmioApiDocBu
``@OA\Response(..., @OA\JsonContent(...))`` or ``@OA\Response(..., @OA\XmlContent(...))``.
When you use ``@Model`` directly (``@OA\Response(..., @Model(...)))``), the media type is set by default to ``json``.
BC Breaks
---------
There are also BC breaks that were introduced in 4.0:
- We migrated from `EXSyst\Component\Swagger\Swagger` to `OpenApi\Annotations\OpenApi` to describe the api in all our describers signature (`DescriberInterface`, `RouteDescriberInterface`, `ModelDescriberInterface`, `PropertyDescriberInterface`).
- `PropertyDescriberInterface` now takes several types as input to leverage compound types support in OpenApi 3.0 (`int|string` for instance).