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
This commit is contained in:
Asmir Mustafic 2019-05-02 10:02:16 +02:00 committed by Guilhem N
parent 101648bb8f
commit eb255010a0
10 changed files with 192 additions and 18 deletions

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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()
{
}
}

View File

@ -0,0 +1,35 @@
<?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\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\BazingaUserTyped;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
/**
* @Route(host="api.example.com")
*/
class BazingaTypedController
{
/**
* @Route("/api/bazinga_typed", methods={"GET"})
* @SWG\Response(
* response=200,
* description="Success",
* @Model(type=BazingaUserTyped::class)
* )
*/
public function userTypedAction()
{
}
}

View File

@ -18,7 +18,21 @@ use Hateoas\Configuration\Annotation as Hateoas;
*
* @Hateoas\Relation(name="example", attributes={"str_att":"bar", "float_att":5.6, "bool_att": false}, href="http://www.example.com")
* @Hateoas\Relation(name="route", href=@Hateoas\Route("foo"))
* @Hateoas\Relation(name="route", attributes={"foo":"bar"}, embedded=@Hateoas\Embedded("expr(service('xx'))"))
* @Hateoas\Relation(
* name="route",
* attributes={"foo":"bar"},
* embedded=@Hateoas\Embedded(
* "expr(service('xx'))"
* )
* )
* @Hateoas\Relation(
* name="embed_with_group",
* attributes={"foo":"with_groups"},
* exclusion=@Hateoas\Exclusion(groups={"foo"}),
* embedded=@Hateoas\Embedded(
* "expr(service('xx'))"
* )
* )
*/
class BazingaUser
{

View File

@ -0,0 +1,34 @@
<?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\Tests\Functional\EntityExcluded;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* name="typed_bazinga_users",
* embedded=@Hateoas\Embedded(
* "expr(service('zz'))",
* type="array<Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser>"
* )
* )
* @Hateoas\Relation(
* name="typed_bazinga_name",
* embedded=@Hateoas\Embedded(
* "expr(service('yy'))",
* type="string"
* )
* )
*/
class BazingaUserTyped
{
}

View File

@ -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];

View File

@ -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'],
],
],
],
]);

View File

@ -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\\": ""