From eb255010a0ba8e83d23dfcf33df9e0af6e7b0533 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Thu, 2 May 2019 10:02:16 +0200 Subject: [PATCH] Support typed embedded relation with willdurand/hateoas 3.0 (#1510) * allow typed embedded relation with hateoas 3.0 * symfony/framework-bundle 4.2.7 is broken https://github.com/symfony/symfony/pull/31156 * internal public methods --- .../BazingaHateoasModelDescriber.php | 35 +++++++------ ModelDescriber/JMSModelDescriber.php | 11 +++- Tests/Functional/BazingaFunctionalTest.php | 50 +++++++++++++++++++ .../Controller/BazingaController.php | 12 +++++ .../Controller/BazingaTypedController.php | 35 +++++++++++++ Tests/Functional/Entity/BazingaUser.php | 16 +++++- .../EntityExcluded/BazingaUserTyped.php | 34 +++++++++++++ Tests/Functional/SwaggerUiTest.php | 1 + Tests/Functional/TestKernel.php | 13 +++++ composer.json | 3 ++ 10 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 Tests/Functional/Controller/BazingaTypedController.php create mode 100644 Tests/Functional/EntityExcluded/BazingaUserTyped.php diff --git a/ModelDescriber/BazingaHateoasModelDescriber.php b/ModelDescriber/BazingaHateoasModelDescriber.php index e967f1a..b19710e 100644 --- a/ModelDescriber/BazingaHateoasModelDescriber.php +++ b/ModelDescriber/BazingaHateoasModelDescriber.php @@ -12,10 +12,9 @@ namespace Nelmio\ApiDocBundle\ModelDescriber; use EXSyst\Component\Swagger\Schema; +use Hateoas\Configuration\Metadata\ClassMetadata; use Hateoas\Configuration\Relation; use Hateoas\Serializer\Metadata\RelationPropertyMetadata; -use JMS\Serializer\Exclusion\GroupsExclusionStrategy; -use JMS\Serializer\SerializationContext; use Metadata\MetadataFactoryInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; @@ -48,39 +47,43 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi { $this->JMSModelDescriber->describe($model, $schema); + /** + * @var ClassMetadata + */ $metadata = $this->getHateoasMetadata($model); if (null === $metadata) { return; } - $groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null; - $schema->setType('object'); + $context = $this->JMSModelDescriber->getSerializationContext($model); foreach ($metadata->getRelations() as $relation) { if (!$relation->getEmbedded() && !$relation->getHref()) { continue; } + $item = new RelationPropertyMetadata($relation->getExclusion(), $relation); - if (null !== $groupsExclusion && $relation->getExclusion()) { - $item = new RelationPropertyMetadata($relation->getExclusion(), $relation); - - // filter groups - if ($groupsExclusion->shouldSkipProperty($item, SerializationContext::create())) { - continue; - } + if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) { + continue; } - $name = $relation->getName(); + $context->pushPropertyMetadata($item); - $relationSchema = $schema->getProperties()->get($relation->getEmbedded() ? '_embedded' : '_links'); + $embedded = $relation->getEmbedded(); + $relationSchema = $schema->getProperties()->get($embedded ? '_embedded' : '_links'); $properties = $relationSchema->getProperties(); $relationSchema->setReadOnly(true); + $name = $relation->getName(); $property = $properties->get($name); - $property->setType('object'); + if ($embedded && method_exists($embedded, 'getType') && $embedded->getType()) { + $this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context); + } else { + $property->setType('object'); + } if ($relation->getHref()) { $subProperties = $property->getProperties(); @@ -89,6 +92,8 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi $this->setAttributeProperties($relation, $subProperties); } + + $context->popPropertyMetadata(); } } @@ -119,7 +124,7 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi foreach ($relation->getAttributes() as $attribute => $value) { $subSubProp = $subProperties->get($attribute); switch (gettype($value)) { - case 'integer': + case 'integer' : $subSubProp->setType('integer'); $subSubProp->setDefault($value); diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index 3e9912b..a00e507 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -118,7 +118,10 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn $context->popClassMetadata(); } - private function getSerializationContext(Model $model): SerializationContext + /** + * @internal + */ + public function getSerializationContext(Model $model): SerializationContext { if (isset($this->contexts[$model->getHash()])) { $context = $this->contexts[$model->getHash()]; @@ -178,7 +181,11 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn return false; } - private function describeItem(array $type, $property, Context $context) + /** + * @internal + * @return void + */ + public function describeItem(array $type, $property, Context $context) { $nestedTypeInfo = $this->getNestedTypeInArray($type); if (null !== $nestedTypeInfo) { diff --git a/Tests/Functional/BazingaFunctionalTest.php b/Tests/Functional/BazingaFunctionalTest.php index ad24421..2432cd9 100644 --- a/Tests/Functional/BazingaFunctionalTest.php +++ b/Tests/Functional/BazingaFunctionalTest.php @@ -11,6 +11,8 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; +use Hateoas\Configuration\Embedded; + class BazingaFunctionalTest extends WebTestCase { public function testModelComplexDocumentationBazinga() @@ -57,12 +59,60 @@ class BazingaFunctionalTest extends WebTestCase 'route' => [ 'type' => 'object', ], + 'embed_with_group' => [ + 'type' => 'object', + ], ], ], ], ], $this->getModel('BazingaUser')->toArray()); } + public function testWithGroup() + { + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + '_embedded' => [ + 'readOnly' => true, + 'properties' => [ + 'embed_with_group' => [ + 'type' => 'object', + ], + ], + ], + ], + ], $this->getModel('BazingaUser_grouped')->toArray()); + } + + public function testWithType() + { + try { + new \ReflectionMethod(Embedded::class, 'getType'); + } catch (\ReflectionException $e) { + $this->markTestSkipped('Typed embedded properties require at least willdurand/hateoas 3.0'); + } + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + '_embedded' => [ + 'readOnly' => true, + 'properties' => [ + 'typed_bazinga_users' => [ + 'items' => [ + '$ref' => '#/definitions/BazingaUser', + ], + 'type' => 'array', + ], + 'typed_bazinga_name' => [ + 'type' => 'string', + ], + ], + ], + ], + ], $this->getModel('BazingaUserTyped')->toArray()); + } + protected static function createKernel(array $options = []) { return new TestKernel(true, true); diff --git a/Tests/Functional/Controller/BazingaController.php b/Tests/Functional/Controller/BazingaController.php index e91fc03..c5c9241 100644 --- a/Tests/Functional/Controller/BazingaController.php +++ b/Tests/Functional/Controller/BazingaController.php @@ -32,4 +32,16 @@ class BazingaController public function userAction() { } + + /** + * @Route("/api/bazinga_foo", methods={"GET"}) + * @SWG\Response( + * response=200, + * description="Success", + * @Model(type=BazingaUser::class, groups={"foo"}) + * ) + */ + public function userGroupAction() + { + } } diff --git a/Tests/Functional/Controller/BazingaTypedController.php b/Tests/Functional/Controller/BazingaTypedController.php new file mode 100644 index 0000000..8592da1 --- /dev/null +++ b/Tests/Functional/Controller/BazingaTypedController.php @@ -0,0 +1,35 @@ +" + * ) + * ) + * @Hateoas\Relation( + * name="typed_bazinga_name", + * embedded=@Hateoas\Embedded( + * "expr(service('yy'))", + * type="string" + * ) + * ) + */ +class BazingaUserTyped +{ +} diff --git a/Tests/Functional/SwaggerUiTest.php b/Tests/Functional/SwaggerUiTest.php index b3cbd81..86f9256 100644 --- a/Tests/Functional/SwaggerUiTest.php +++ b/Tests/Functional/SwaggerUiTest.php @@ -54,6 +54,7 @@ class SwaggerUiTest extends WebTestCase 'Dummy' => $expected['definitions']['Dummy'], 'Test' => ['type' => 'string'], 'JMSPicture_mini' => ['type' => 'object'], + 'BazingaUser_grouped' => ['type' => 'object'], ]; yield ['/docs/test', 'test', $expected]; diff --git a/Tests/Functional/TestKernel.php b/Tests/Functional/TestKernel.php index 912fe0c..43d529b 100644 --- a/Tests/Functional/TestKernel.php +++ b/Tests/Functional/TestKernel.php @@ -14,8 +14,10 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle; use Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle; use FOS\RestBundle\FOSRestBundle; +use Hateoas\Configuration\Embedded; use JMS\SerializerBundle\JMSSerializerBundle; use Nelmio\ApiDocBundle\NelmioApiDocBundle; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture; use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber; use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle; @@ -88,6 +90,12 @@ class TestKernel extends Kernel if ($this->useBazinga) { $routes->import(__DIR__.'/Controller/BazingaController.php', '/', 'annotation'); + + try { + new \ReflectionMethod(Embedded::class, 'getType'); + $routes->import(__DIR__.'/Controller/BazingaTypedController.php', '/', 'annotation'); + } catch (\ReflectionException $e) { + } } } @@ -172,6 +180,11 @@ class TestKernel extends Kernel 'type' => JMSPicture::class, 'groups' => ['mini'], ], + [ + 'alias' => 'BazingaUser_grouped', + 'type' => BazingaUser::class, + 'groups' => ['foo'], + ], ], ], ]); diff --git a/composer.json b/composer.json index 7da0441..95aede5 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,9 @@ "api-platform/core": "For using an API oriented framework.", "friendsofsymfony/rest-bundle": "For using the parameters annotations." }, + "conflict": { + "symfony/framework-bundle": "4.2.7" + }, "autoload": { "psr-4": { "Nelmio\\ApiDocBundle\\": ""