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']) {
$container->register('nelmio_api_doc.model_describers.jms', JMSModelDescriber::class)
->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]);
}

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;
private $factory;
private $namingStrategy;
public function __construct(MetadataFactoryInterface $factory, PropertyNamingStrategyInterface $namingStrategy)
private $swaggerPropertyAnnotationReader;
public function __construct(
MetadataFactoryInterface $factory,
PropertyNamingStrategyInterface $namingStrategy,
SwaggerPropertyAnnotationReader $swaggerPropertyAnnotationReader
)
{
$this->factory = $factory;
$this->namingStrategy = $namingStrategy;
$this->swaggerPropertyAnnotationReader = $swaggerPropertyAnnotationReader;
}
/**
@ -73,12 +81,12 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
$type = $item->type['name'];
}
if (in_array($type, array('boolean', 'integer', 'string', 'array'))) {
if (in_array($type, ['boolean', 'integer', 'string', 'array'])) {
$property->setType($type);
} elseif ('double' === $type || 'float' === $type) {
} elseif (in_array($type, ['double', 'float'])) {
$property->setType('number');
$property->setFormat($type);
} elseif ('DateTime' === $type || 'DateTimeImmutable' === $type) {
} elseif (in_array($type, ['DateTime', 'DateTimeImmutable'])) {
$property->setType('string');
$property->setFormat('date-time');
} 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()))
);
}
// 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;
public function __construct(PropertyInfoExtractorInterface $propertyInfo)
private $swaggerPropertyAnnotationReader;
public function __construct(
PropertyInfoExtractorInterface $propertyInfo,
SwaggerPropertyAnnotationReader $swaggerPropertyAnnotationReader
)
{
$this->propertyInfo = $propertyInfo;
$this->swaggerPropertyAnnotationReader = $swaggerPropertyAnnotationReader;
}
public function describe(Model $model, Schema $schema)
@ -54,9 +60,44 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
throw new \LogicException(sprintf('Property %s::$%s defines more than one type.', $class, $propertyName));
}
$properties->get($propertyName)->setRef(
$this->modelRegistry->register(new Model($types[0], $model->getGroups()))
);
$type = $types[0];
$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
);
}
}
}

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>
<!-- Model Describers -->
<service id="nelmio_api_doc.model_describers.collection" class="Nelmio\ApiDocBundle\ModelDescriber\CollectionModelDescriber" public="false">
<tag name="nelmio_api_doc.model_describer" priority="10" />
<service id="nelmio_api_doc.model_describers.swagger_property_annotation_reader" class="Nelmio\ApiDocBundle\ModelDescriber\SwaggerPropertyAnnotationReader" public="false">
<argument type="service" id="annotation_reader" />
</service>
<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="nelmio_api_doc.model_describers.swagger_property_annotation_reader" />
<tag name="nelmio_api_doc.model_describer" />
</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>
</container>

View File

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

View File

@ -11,11 +11,39 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use Swagger\Annotations as SWG;
/**
* @author Guilhem N. <egetick@gmail.com>
*/
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
*/
@ -26,6 +54,38 @@ class User
*/
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)
{
}

View File

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

View File

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