use jms serialization groups detection

This commit is contained in:
Asmir Mustafic 2019-03-11 12:53:35 +01:00
parent ccad10aae1
commit 65e940f7f8
No known key found for this signature in database
GPG Key ID: 5408354D09CC64FC
6 changed files with 99 additions and 87 deletions

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use EXSyst\Component\Swagger\Schema; use EXSyst\Component\Swagger\Schema;
use JMS\Serializer\Context;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy; use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface; use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializationContext;
@ -31,9 +32,14 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
use ModelRegistryAwareTrait; use ModelRegistryAwareTrait;
private $factory; private $factory;
private $namingStrategy; private $namingStrategy;
private $doctrineReader; private $doctrineReader;
private $previousGroups = [];
private $contexts = [];
private $metadataStacks = [];
/** /**
* @var array * @var array
@ -61,40 +67,22 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className)); throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
} }
$groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null;
$schema->setType('object'); $schema->setType('object');
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry); $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
$annotationsReader->updateDefinition(new \ReflectionClass($className), $schema); $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
$isJmsV1 = null !== $this->namingStrategy; $isJmsV1 = null !== $this->namingStrategy;
$properties = $schema->getProperties(); $properties = $schema->getProperties();
$context = $this->getSerializationContext($model);
$context->pushClassMetadata($metadata);
foreach ($metadata->propertyMetadata as $item) { foreach ($metadata->propertyMetadata as $item) {
// filter groups // filter groups
if (null !== $groupsExclusion && $groupsExclusion->shouldSkipProperty($item, SerializationContext::create())) { if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) {
continue; continue;
} }
$groups = $model->getGroups(); $context->pushPropertyMetadata($item);
$previousGroups = null;
if (isset($groups[$item->name]) && is_array($groups[$item->name])) {
$previousGroups = $groups;
$groups = $groups[$item->name];
} elseif (!isset($groups[$item->name]) && !empty($this->previousGroups[$model->getHash()])) {
$groups = false === $this->propertyTypeUsesGroups($item->type)
? null
: ($isJmsV1 ? [GroupsExclusionStrategy::DEFAULT_GROUP] : $this->previousGroups[$model->getHash()]);
}
if (is_array($groups)) {
$groups = array_filter($groups, 'is_scalar');
}
if ([GroupsExclusionStrategy::DEFAULT_GROUP] === $groups) {
$groups = null;
}
$name = true === $isJmsV1 ? $this->namingStrategy->translateName($item) : $item->serializedName; $name = true === $isJmsV1 ? $this->namingStrategy->translateName($item) : $item->serializedName;
// read property options from Swagger Property annotation if it exists // read property options from Swagger Property annotation if it exists
@ -106,22 +94,71 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
} }
$property = $properties->get($annotationsReader->getPropertyName($reflection, $name)); $property = $properties->get($annotationsReader->getPropertyName($reflection, $name));
$groups = $this->computeGroups($context, $item->type);
$annotationsReader->updateProperty($reflection, $property, $groups); $annotationsReader->updateProperty($reflection, $property, $groups);
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
$property = $properties->get($name); $property = $properties->get($name);
} }
if (null !== $property->getType() || null !== $property->getRef()) { if (null !== $property->getType() || null !== $property->getRef()) {
$context->popPropertyMetadata();
continue; continue;
} }
if (null === $item->type) { if (null === $item->type) {
$properties->remove($name); $properties->remove($name);
$context->popPropertyMetadata();
continue; continue;
} }
$this->describeItem($item->type, $property, $groups, $previousGroups); $this->describeItem($item->type, $property, $context, $item);
$context->popPropertyMetadata();
} }
$context->popClassMetadata();
}
private function getSerializationContext(Model $model): SerializationContext
{
if (isset($this->contexts[$model->getHash()])) {
$context = $this->contexts[$model->getHash()];
$stack = $context->getMetadataStack();
while (!$stack->isEmpty()) {
$stack->pop();
}
foreach ($this->metadataStacks[$model->getHash()] as $metadataCopy) {
$stack->unshift($metadataCopy);
}
} else {
$context = SerializationContext::create();
if (null !== $model->getGroups()) {
$context->addExclusionStrategy(new GroupsExclusionStrategy($model->getGroups()));
}
}
return $context;
}
private function computeGroups(Context $context, array $type = null)
{
if (null === $type || true !== $this->propertyTypeUsesGroups($type)) {
return null;
}
$groupsExclusion = $context->getExclusionStrategy();
if (!($groupsExclusion instanceof GroupsExclusionStrategy)) {
return null;
}
$groups = $groupsExclusion->getGroupsFor($context);
if ([GroupsExclusionStrategy::DEFAULT_GROUP] === $groups) {
return null;
}
return $groups;
} }
/** /**
@ -141,7 +178,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
return false; return false;
} }
private function describeItem(array $type, $property, array $groups = null, array $previousGroups = null) private function describeItem(array $type, $property, Context $context)
{ {
$nestedTypeInfo = $this->getNestedTypeInArray($type); $nestedTypeInfo = $this->getNestedTypeInArray($type);
if (null !== $nestedTypeInfo) { if (null !== $nestedTypeInfo) {
@ -156,13 +193,13 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
return; return;
} }
$this->describeItem($nestedType, $property->getAdditionalProperties(), $groups, $previousGroups); $this->describeItem($nestedType, $property->getAdditionalProperties(), $context);
return; return;
} }
$property->setType('array'); $property->setType('array');
$this->describeItem($nestedType, $property->getItems(), $groups, $previousGroups); $this->describeItem($nestedType, $property->getItems(), $context);
} elseif ('array' === $type['name']) { } elseif ('array' === $type['name']) {
$property->setType('object'); $property->setType('object');
$property->merge(['additionalProperties' => []]); $property->merge(['additionalProperties' => []]);
@ -177,12 +214,13 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
$property->setType('string'); $property->setType('string');
$property->setFormat('date-time'); $property->setFormat('date-time');
} else { } else {
$groups = $this->computeGroups($context, $type);
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups); $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups);
$property->setRef($this->modelRegistry->register($model)); $property->setRef($this->modelRegistry->register($model));
if ($previousGroups) { $this->contexts[$model->getHash()] = $context;
$this->previousGroups[$model->getHash()] = $previousGroups; $this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack();
}
} }
} }

View File

@ -30,7 +30,7 @@ class JMSChatFriend
/** /**
* @Serializer\Type("Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatLivingRoom") * @Serializer\Type("Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatLivingRoom")
* @Serializer\Expose * @Serializer\Expose
* @Serializer\Groups({"Default"}) * @Serializer\Groups({"Default", "mini"})
*/ */
private $living; private $living;

View File

@ -28,7 +28,7 @@ class JMSChatUser
/** /**
* @Serializer\Type("Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture") * @Serializer\Type("Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture")
* @Serializer\Groups({"Default", "mini"}) * @Serializer\Groups({"mini"})
* @Serializer\Expose * @Serializer\Expose
*/ */
private $picture; private $picture;

View File

@ -11,8 +11,6 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
class JMSFunctionalTest extends WebTestCase class JMSFunctionalTest extends WebTestCase
{ {
public function testModelPictureDocumentation() public function testModelPictureDocumentation()
@ -20,11 +18,20 @@ class JMSFunctionalTest extends WebTestCase
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'only_direct_picture_mini' => [ 'id' => [
'type' => 'integer', 'type' => 'integer',
], ],
], ],
], $this->getModel('JMSPicture')->toArray()); ], $this->getModel('JMSPicture')->toArray());
$this->assertEquals([
'type' => 'object',
'properties' => [
'only_direct_picture_mini' => [
'type' => 'integer',
],
],
], $this->getModel('JMSPicture_mini')->toArray());
} }
public function testModeChatDocumentation() public function testModeChatDocumentation()
@ -48,7 +55,7 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'picture' => [ 'picture' => [
'$ref' => '#/definitions/JMSPicture2', '$ref' => '#/definitions/JMSPicture',
], ],
], ],
], $this->getModel('JMSChatUser')->toArray()); ], $this->getModel('JMSChatUser')->toArray());
@ -212,12 +219,8 @@ class JMSFunctionalTest extends WebTestCase
], $this->getModel('JMSDualComplex')->toArray()); ], $this->getModel('JMSDualComplex')->toArray());
} }
public function testNestedGroupsV1() public function testNestedGroups()
{ {
if (interface_exists(SerializationVisitorInterface::class)){
$this->markTestSkipped('This applies only for jms/serializer v1.x');
}
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
@ -230,48 +233,18 @@ class JMSFunctionalTest extends WebTestCase
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'id1' => ['type' => 'integer'], 'id1' => ['type' => 'integer'],
'id2' => ['type' => 'integer'],
'id3' => ['type' => 'integer'], 'id3' => ['type' => 'integer'],
], ],
], $this->getModel('JMSChatRoom')->toArray()); ], $this->getModel('JMSChatRoom')->toArray());
} }
public function testNestedGroupsV2()
{
if (!interface_exists(SerializationVisitorInterface::class)){
$this->markTestSkipped('This applies only for jms/serializer v2.x');
}
$this->assertEquals([
'type' => 'object',
'properties' => [
'living' => ['$ref' => '#/definitions/JMSChatLivingRoom'],
'dining' => ['$ref' => '#/definitions/JMSChatRoom'],
],
], $this->getModel('JMSChatFriend')->toArray());
$this->assertEquals([
'type' => 'object',
'properties' => [
'id2' => ['type' => 'integer'],
],
], $this->getModel('JMSChatRoom')->toArray());
$this->assertEquals([
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
],
], $this->getModel('JMSChatLivingRoom')->toArray());
}
public function testModelComplexDocumentation() public function testModelComplexDocumentation()
{ {
$this->assertEquals([ $this->assertEquals([
'type' => 'object', 'type' => 'object',
'properties' => [ 'properties' => [
'id' => ['type' => 'integer'], 'id' => ['type' => 'integer'],
'user' => ['$ref' => '#/definitions/JMSUser2'], 'user' => ['$ref' => '#/definitions/JMSUser'],
'name' => ['type' => 'string'], 'name' => ['type' => 'string'],
'virtual' => ['$ref' => '#/definitions/JMSUser'], 'virtual' => ['$ref' => '#/definitions/JMSUser'],
], ],
@ -280,19 +253,6 @@ class JMSFunctionalTest extends WebTestCase
'user', 'user',
], ],
], $this->getModel('JMSComplex')->toArray()); ], $this->getModel('JMSComplex')->toArray());
$this->assertEquals([
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'title' => 'userid',
'description' => 'User id',
'readOnly' => true,
'example' => '1',
],
],
], $this->getModel('JMSUser2')->toArray());
} }
public function testYamlConfig() public function testYamlConfig()

View File

@ -50,7 +50,11 @@ class SwaggerUiTest extends WebTestCase
'responses' => ['200' => ['description' => 'Test']], 'responses' => ['200' => ['description' => 'Test']],
]], ]],
]; ];
$expected['definitions'] = ['Dummy' => $expected['definitions']['Dummy'], 'Test' => ['type' => 'string']]; $expected['definitions'] = [
'Dummy' => $expected['definitions']['Dummy'],
'Test' => ['type' => 'string'],
'JMSPicture_mini' => ['type' => 'object'],
];
yield ['/docs/test', 'test', $expected]; yield ['/docs/test', 'test', $expected];
} }

View File

@ -16,6 +16,7 @@ use Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle;
use FOS\RestBundle\FOSRestBundle; use FOS\RestBundle\FOSRestBundle;
use JMS\SerializerBundle\JMSSerializerBundle; use JMS\SerializerBundle\JMSSerializerBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle; use Nelmio\ApiDocBundle\NelmioApiDocBundle;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber; use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber;
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle; use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
@ -164,6 +165,15 @@ class TestKernel extends Kernel
], ],
], ],
], ],
'models' => [
'names' => [
[
'alias' => 'JMSPicture_mini',
'type' => JMSPicture::class,
'groups' => ['mini'],
],
],
],
]); ]);
$def = new Definition(VirtualTypeClassDoesNotExistsHandlerDefinedDescriber::class); $def = new Definition(VirtualTypeClassDoesNotExistsHandlerDefinedDescriber::class);