From 01f691c4568de54063f580110247646e300f7dd6 Mon Sep 17 00:00:00 2001 From: Myroslav Date: Sun, 3 Dec 2017 20:30:44 +0200 Subject: [PATCH] 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 --- DependencyInjection/NelmioApiDocExtension.php | 6 +- ModelDescriber/CollectionModelDescriber.php | 35 ---------- ModelDescriber/DateTimeModelDescriber.php | 29 --------- ModelDescriber/JMSModelDescriber.php | 19 ++++-- ModelDescriber/ObjectModelDescriber.php | 49 ++++++++++++-- ModelDescriber/ScalarModelDescriber.php | 37 ----------- .../SwaggerPropertyAnnotationReader.php | 56 ++++++++++++++++ Resources/config/services.xml | 17 +---- Tests/Functional/Entity/JMSUser.php | 14 ++++ Tests/Functional/Entity/User.php | 60 +++++++++++++++++ Tests/Functional/FunctionalTest.php | 64 +++++++++++-------- Tests/Functional/JMSFunctionalTest.php | 16 ++++- 12 files changed, 251 insertions(+), 151 deletions(-) delete mode 100644 ModelDescriber/CollectionModelDescriber.php delete mode 100644 ModelDescriber/DateTimeModelDescriber.php delete mode 100644 ModelDescriber/ScalarModelDescriber.php create mode 100644 ModelDescriber/SwaggerPropertyAnnotationReader.php diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index bd541d6..6f05139 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -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]); } diff --git a/ModelDescriber/CollectionModelDescriber.php b/ModelDescriber/CollectionModelDescriber.php deleted file mode 100644 index d238b26..0000000 --- a/ModelDescriber/CollectionModelDescriber.php +++ /dev/null @@ -1,35 +0,0 @@ -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(); - } -} diff --git a/ModelDescriber/DateTimeModelDescriber.php b/ModelDescriber/DateTimeModelDescriber.php deleted file mode 100644 index 36fbfaa..0000000 --- a/ModelDescriber/DateTimeModelDescriber.php +++ /dev/null @@ -1,29 +0,0 @@ -setType('string'); - $schema->setFormat('date-time'); - } - - public function supports(Model $model): bool - { - return 'DateTime' === $model->getType()->getClassName() || 'DateTimeImmutable' === $model->getType()->getClassName(); - } -} diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index c55eaed..d03c465 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -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); } } diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 469cfc5..cbd834c 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -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 + ); + } } } diff --git a/ModelDescriber/ScalarModelDescriber.php b/ModelDescriber/ScalarModelDescriber.php deleted file mode 100644 index 2ce8a86..0000000 --- a/ModelDescriber/ScalarModelDescriber.php +++ /dev/null @@ -1,37 +0,0 @@ - '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()]); - } -} diff --git a/ModelDescriber/SwaggerPropertyAnnotationReader.php b/ModelDescriber/SwaggerPropertyAnnotationReader.php new file mode 100644 index 0000000..e272b98 --- /dev/null +++ b/ModelDescriber/SwaggerPropertyAnnotationReader.php @@ -0,0 +1,56 @@ +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); + } + } + } +} diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 5606cd4..00443e4 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -50,27 +50,16 @@ - - + + + - - - - - - - - - - - - diff --git a/Tests/Functional/Entity/JMSUser.php b/Tests/Functional/Entity/JMSUser.php index c173441..fa24ee6 100644 --- a/Tests/Functional/Entity/JMSUser.php +++ b/Tests/Functional/Entity/JMSUser.php @@ -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 diff --git a/Tests/Functional/Entity/User.php b/Tests/Functional/Entity/User.php index ff69567..abd5ae1 100644 --- a/Tests/Functional/Entity/User.php +++ b/Tests/Functional/Entity/User.php @@ -11,11 +11,39 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; +use Swagger\Annotations as SWG; + /** * @author Guilhem N. */ 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) { } diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 4bbd662..0bb4a71 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -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() diff --git a/Tests/Functional/JMSFunctionalTest.php b/Tests/Functional/JMSFunctionalTest.php index 1f111eb..4995ad0 100644 --- a/Tests/Functional/JMSFunctionalTest.php +++ b/Tests/Functional/JMSFunctionalTest.php @@ -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' => [