mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-02 15:51:48 +03:00
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:
parent
d558560f61
commit
d932b06bbb
@ -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']);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
54
PropertyDescriber/CompoundPropertyDescriber.php
Normal file
54
PropertyDescriber/CompoundPropertyDescriber.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
25
PropertyDescriber/NullablePropertyTrait.php
Normal file
25
PropertyDescriber/NullablePropertyTrait.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
20
Tests/Functional/Entity/CompoundEntity.php
Normal file
20
Tests/Functional/Entity/CompoundEntity.php
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
Loading…
x
Reference in New Issue
Block a user