support swagger property annotation to extend description properties of model (#1125)

* support swagger property annotation to descripe properties of model

* support swagger property annotation to descripe properties of model

* fix issues from PR review

* rename method

* remove redundant annotations and revert changes into composer.json

* fix issues from PR comments

* use symfony 3 for default tests

* revert chages

* use symfony 3 for default tests

* revert changes in travis config
This commit is contained in:
Myroslav 2017-12-03 20:30:44 +02:00 committed by Guilhem N
parent 8a023a1897
commit 01f691c456
12 changed files with 251 additions and 151 deletions

View File

@ -99,7 +99,11 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
if ($config['models']['use_jms']) { if ($config['models']['use_jms']) {
$container->register('nelmio_api_doc.model_describers.jms', JMSModelDescriber::class) $container->register('nelmio_api_doc.model_describers.jms', JMSModelDescriber::class)
->setPublic(false) ->setPublic(false)
->setArguments([new Reference('jms_serializer.metadata_factory'), new Reference('jms_serializer.naming_strategy')]) ->setArguments([
new Reference('jms_serializer.metadata_factory'),
new Reference('jms_serializer.naming_strategy'),
new Reference('nelmio_api_doc.model_describers.swagger_property_annotation_reader'),
])
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]); ->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);
} }

View File

@ -1,35 +0,0 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
class CollectionModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
public function describe(Model $model, Schema $schema)
{
$schema->setType('array');
$schema->getItems()->setRef(
$this->modelRegistry->register(new Model($model->getType()->getCollectionValueType(), $model->getGroups()))
);
}
public function supports(Model $model): bool
{
return $model->getType()->isCollection() && null !== $model->getType()->getCollectionValueType();
}
}

View File

@ -1,29 +0,0 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
class DateTimeModelDescriber implements ModelDescriberInterface
{
public function describe(Model $model, Schema $schema)
{
$schema->setType('string');
$schema->setFormat('date-time');
}
public function supports(Model $model): bool
{
return 'DateTime' === $model->getType()->getClassName() || 'DateTimeImmutable' === $model->getType()->getClassName();
}
}

View File

@ -30,12 +30,20 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
use ModelRegistryAwareTrait; use ModelRegistryAwareTrait;
private $factory; private $factory;
private $namingStrategy; private $namingStrategy;
public function __construct(MetadataFactoryInterface $factory, PropertyNamingStrategyInterface $namingStrategy) private $swaggerPropertyAnnotationReader;
public function __construct(
MetadataFactoryInterface $factory,
PropertyNamingStrategyInterface $namingStrategy,
SwaggerPropertyAnnotationReader $swaggerPropertyAnnotationReader
)
{ {
$this->factory = $factory; $this->factory = $factory;
$this->namingStrategy = $namingStrategy; $this->namingStrategy = $namingStrategy;
$this->swaggerPropertyAnnotationReader = $swaggerPropertyAnnotationReader;
} }
/** /**
@ -73,12 +81,12 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
$type = $item->type['name']; $type = $item->type['name'];
} }
if (in_array($type, array('boolean', 'integer', 'string', 'array'))) { if (in_array($type, ['boolean', 'integer', 'string', 'array'])) {
$property->setType($type); $property->setType($type);
} elseif ('double' === $type || 'float' === $type) { } elseif (in_array($type, ['double', 'float'])) {
$property->setType('number'); $property->setType('number');
$property->setFormat($type); $property->setFormat($type);
} elseif ('DateTime' === $type || 'DateTimeImmutable' === $type) { } elseif (in_array($type, ['DateTime', 'DateTimeImmutable'])) {
$property->setType('string'); $property->setType('string');
$property->setFormat('date-time'); $property->setFormat('date-time');
} else { } else {
@ -91,6 +99,9 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
$this->modelRegistry->register(new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type), $model->getGroups())) $this->modelRegistry->register(new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type), $model->getGroups()))
); );
} }
// read property options from Swagger Property annotation if it exists
$this->swaggerPropertyAnnotationReader->updateWithSwaggerPropertyAnnotation($item->reflection, $property);
} }
} }

View File

@ -24,9 +24,15 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
private $propertyInfo; private $propertyInfo;
public function __construct(PropertyInfoExtractorInterface $propertyInfo) private $swaggerPropertyAnnotationReader;
public function __construct(
PropertyInfoExtractorInterface $propertyInfo,
SwaggerPropertyAnnotationReader $swaggerPropertyAnnotationReader
)
{ {
$this->propertyInfo = $propertyInfo; $this->propertyInfo = $propertyInfo;
$this->swaggerPropertyAnnotationReader = $swaggerPropertyAnnotationReader;
} }
public function describe(Model $model, Schema $schema) public function describe(Model $model, Schema $schema)
@ -54,10 +60,45 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
throw new \LogicException(sprintf('Property %s::$%s defines more than one type.', $class, $propertyName)); throw new \LogicException(sprintf('Property %s::$%s defines more than one type.', $class, $propertyName));
} }
$properties->get($propertyName)->setRef( $type = $types[0];
$this->modelRegistry->register(new Model($types[0], $model->getGroups())) $property = $properties->get($propertyName);
if (Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType()) {
$type = $type->getCollectionValueType();
$property->setType('array');
$property = $property->getItems();
}
if ($type->getBuiltinType() === Type::BUILTIN_TYPE_STRING) {
$property->setType('string');
} elseif ($type->getBuiltinType() === Type::BUILTIN_TYPE_BOOL) {
$property->setType('boolean');
} elseif ($type->getBuiltinType() === Type::BUILTIN_TYPE_INT) {
$property->setType('integer');
} elseif ($type->getBuiltinType() === Type::BUILTIN_TYPE_FLOAT) {
$property->setType('number');
$property->setFormat('float');
} elseif ($type->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT) {
if (in_array($type->getClassName(), ['DateTime', 'DateTimeImmutable'])) {
$property->setType('string');
$property->setFormat('date-time');
} else {
$property->setRef(
$this->modelRegistry->register(new Model($type, $model->getGroups()))
); );
} }
} else {
throw new \Exception(sprintf("Unknown type: %s", $type->getBuiltinType()));
}
// read property options from Swagger Property annotation if it exists
if (property_exists($class, $propertyName)) {
$this->swaggerPropertyAnnotationReader->updateWithSwaggerPropertyAnnotation(
new \ReflectionProperty($class, $propertyName),
$property
);
}
}
} }
public function supports(Model $model): bool public function supports(Model $model): bool

View File

@ -1,37 +0,0 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Model\Model;
use Symfony\Component\PropertyInfo\Type;
class ScalarModelDescriber implements ModelDescriberInterface
{
private static $supportedTypes = [
Type::BUILTIN_TYPE_INT => 'integer',
Type::BUILTIN_TYPE_FLOAT => 'float',
Type::BUILTIN_TYPE_STRING => 'string',
Type::BUILTIN_TYPE_BOOL => 'boolean',
];
public function describe(Model $model, Schema $schema)
{
$type = self::$supportedTypes[$model->getType()->getBuiltinType()];
$schema->setType($type);
}
public function supports(Model $model): bool
{
return isset(self::$supportedTypes[$model->getType()->getBuiltinType()]);
}
}

View File

@ -0,0 +1,56 @@
<?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\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use EXSyst\Component\Swagger\Items;
use Swagger\Annotations\Property as SwgProperty;
use Doctrine\Common\Annotations\Reader;
/**
* @internal
*/
class SwaggerPropertyAnnotationReader
{
private $annotationsReader;
public function __construct(Reader $annotationsReader)
{
$this->annotationsReader = $annotationsReader;
}
/**
* @param \ReflectionProperty $reflectionProperty
* @param Items|Schema $property
*/
public function updateWithSwaggerPropertyAnnotation(\ReflectionProperty $reflectionProperty, $property)
{
$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class);
if ($swgProperty instanceof SwgProperty) {
if ($swgProperty->description !== null) {
$property->setDescription($swgProperty->description);
}
if ($swgProperty->type !== null) {
$property->setType($swgProperty->type);
}
if ($swgProperty->readOnly !== null) {
$property->setReadOnly($swgProperty->readOnly);
}
if ($swgProperty->title !== null) {
$property->setTitle($swgProperty->title);
}
if ($swgProperty->example !== null) {
$property->setExample((string) $swgProperty->example);
}
}
}
}

View File

@ -50,27 +50,16 @@
</service> </service>
<!-- Model Describers --> <!-- Model Describers -->
<service id="nelmio_api_doc.model_describers.collection" class="Nelmio\ApiDocBundle\ModelDescriber\CollectionModelDescriber" public="false"> <service id="nelmio_api_doc.model_describers.swagger_property_annotation_reader" class="Nelmio\ApiDocBundle\ModelDescriber\SwaggerPropertyAnnotationReader" public="false">
<tag name="nelmio_api_doc.model_describer" priority="10" /> <argument type="service" id="annotation_reader" />
</service> </service>
<service id="nelmio_api_doc.model_describers.object" class="Nelmio\ApiDocBundle\ModelDescriber\ObjectModelDescriber" public="false"> <service id="nelmio_api_doc.model_describers.object" class="Nelmio\ApiDocBundle\ModelDescriber\ObjectModelDescriber" public="false">
<argument type="service" id="property_info" /> <argument type="service" id="property_info" />
<argument type="service" id="nelmio_api_doc.model_describers.swagger_property_annotation_reader" />
<tag name="nelmio_api_doc.model_describer" /> <tag name="nelmio_api_doc.model_describer" />
</service> </service>
<service id="nelmio_api_doc.model_describers.datetime" class="Nelmio\ApiDocBundle\ModelDescriber\DateTimeModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" priority="100" />
</service>
<service id="nelmio_api_doc.model_describers.collection" class="Nelmio\ApiDocBundle\ModelDescriber\CollectionModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" />
</service>
<service id="nelmio_api_doc.model_describers.scalar" class="Nelmio\ApiDocBundle\ModelDescriber\ScalarModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" />
</service>
</services> </services>
</container> </container>

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use JMS\Serializer\Annotation as Serializer; use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
/** /**
* User. * User.
@ -23,12 +24,16 @@ class JMSUser
/** /**
* @Serializer\Type("integer") * @Serializer\Type("integer")
* @Serializer\Expose * @Serializer\Expose
*
* @SWG\Property(description = "User id", required = true, readOnly = true, title = "userid", example=1)
*/ */
private $id; private $id;
/** /**
* @Serializer\Type("string") * @Serializer\Type("string")
* @Serializer\Expose * @Serializer\Expose
*
* @SWG\Property(readOnly = false)
*/ */
private $email; private $email;
@ -57,6 +62,15 @@ class JMSUser
*/ */
private $friends; private $friends;
/**
* @Serializer\Type("integer")
* @Serializer\Expose
* @Serializer\SerializedName("friendsNumber")
*
* @SWG\Property(type = "string")
*/
private $friendsNumber;
/** /**
* @Serializer\Type(User::class) * @Serializer\Type(User::class)
* @Serializer\Expose * @Serializer\Expose

View File

@ -11,11 +11,39 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use Swagger\Annotations as SWG;
/** /**
* @author Guilhem N. <egetick@gmail.com> * @author Guilhem N. <egetick@gmail.com>
*/ */
class User class User
{ {
/**
* @var integer
*
* @SWG\Property(description = "User id", required = true, readOnly = true, title = "userid", example=1)
*/
private $id;
/**
* @var string
*
* @SWG\Property(readOnly = false)
*/
private $email;
/**
* @var int
*
* @SWG\Property(type = "string")
*/
private $friendsNumber;
/**
* @var float
*/
private $money;
/** /**
* @var \DateTime * @var \DateTime
*/ */
@ -26,6 +54,38 @@ class User
*/ */
private $users; private $users;
/**
* @param float $money
*/
public function setMoney(float $money)
{
$this->money = $money;
}
/**
* @param int $id
*/
public function setId(int $id)
{
$this->id = $id;
}
/**
* @param string $email
*/
public function setEmail(string $email)
{
$this->email = $email;
}
/**
* @param int $friendsNumber
*/
public function setFriendsNumber(int $friendsNumber)
{
$this->friendsNumber = $friendsNumber;
}
public function setCreatedAt(\DateTime $createAt) public function setCreatedAt(\DateTime $createAt)
{ {
} }

View File

@ -149,31 +149,45 @@ class FunctionalTest extends WebTestCase
public function testUserModel() public function testUserModel()
{ {
$model = $this->getModel('User'); $this->assertEquals(
$this->assertEquals('object', $model->getType()); [
$properties = $model->getProperties(); 'type' => 'object',
$this->assertCount(3, $properties); 'properties' => [
'money' => [
$this->assertTrue($properties->has('users')); 'type' => 'number',
$this->assertEquals('#/definitions/User[]', $properties->get('users')->getRef()); 'format' => 'float',
],
$this->assertTrue($properties->has('dummy')); 'id' => [
$this->assertEquals('#/definitions/Dummy2', $properties->get('dummy')->getRef()); 'type' => 'integer',
'description' => "User id",
$this->assertTrue($properties->has('createdAt')); 'readOnly' => true,
$this->assertEquals('#/definitions/DateTime', $properties->get('createdAt')->getRef()); 'title' => "userid",
'example' => 1,
$model = $this->getModel('DateTime'); ],
$this->assertEquals('string', $model->getType()); 'email' => [
$this->assertEquals('date-time', $model->getFormat()); 'type' => 'string',
} 'readOnly' => false,
],
public function testUsersModel() 'friendsNumber' => [
{ 'type' => 'string',
$model = $this->getModel('User[]'); ],
$this->assertEquals('array', $model->getType()); 'createdAt' => [
'type' => 'string',
$this->assertEquals('#/definitions/User', $model->getItems()->getRef()); 'format' => 'date-time',
],
'users' => [
'items' => [
'$ref' => '#/definitions/User',
],
'type' => 'array',
],
'dummy' => [
'$ref' => '#/definitions/Dummy2',
],
],
],
$this->getModel('User')->toArray()
);
} }
public function testFormSupport() public function testFormSupport()

View File

@ -18,12 +18,24 @@ class JMSFunctionalTest extends WebTestCase
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'id' => ['type' => 'integer'], 'id' => [
'email' => ['type' => 'string'], 'type' => 'integer',
'description' => "User id",
'readOnly' => true,
'title' => "userid",
'example' => 1,
],
'email' => [
'type' => 'string',
'readOnly' => false,
],
'roles' => [ 'roles' => [
'type' => 'array', 'type' => 'array',
'items' => ['type' => 'string'], 'items' => ['type' => 'string'],
], ],
'friendsNumber' => [
'type' => 'string',
],
'friends' => [ 'friends' => [
'type' => 'array', 'type' => 'array',
'items' => [ 'items' => [