Skip to content

Commit

Permalink
feat-2207: Implement property describer for dictionary (#2208)
Browse files Browse the repository at this point in the history
* feat-2207: Implement property describer for dictionary

* feat-2207: style fix

* feat-2207: update baseline

* fix ci fail on php 7.2

* style fix

* fix baseline merge

* update changelog for 4.20.0
  • Loading branch information
DjordyKoert committed Feb 13, 2024
1 parent 2b5d3bd commit 61c08f1
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========

4.20.0
-----
* Added Redocly as an alternative to Swagger UI. https://github.com/Redocly/redoc.
* Added support for describing dictionary types in OpenAPI 3.0.

4.0.0
-----
* Added support of OpenAPI 3.0. The internals were completely reworked and this version introduces BC breaks.
Expand Down
14 changes: 12 additions & 2 deletions PropertyDescriber/ArrayPropertyDescriber.php
Expand Up @@ -15,6 +15,7 @@
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;

class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface, PropertyDescriberAwareInterface
{
Expand All @@ -24,6 +25,7 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
public function describe(array $types, OA\Schema $property, array $groups = null, ?OA\Schema $schema = null, array $context = [])
{
$property->type = 'array';
/** @var OA\Items $property */
$property = Util::getChild($property, OA\Items::class);

foreach ($types[0]->getCollectionValueTypes() as $type) {
Expand All @@ -39,7 +41,15 @@ public function describe(array $types, OA\Schema $property, array $groups = null

public function supports(array $types): bool
{
return 1 === count($types)
&& $types[0]->isCollection();
if (1 !== count($types) || !$types[0]->isCollection()) {
return false;
}

if (empty($types[0]->getCollectionKeyTypes())) {
return true;
}

return 1 === count($types[0]->getCollectionKeyTypes())
&& Type::BUILTIN_TYPE_INT === $types[0]->getCollectionKeyTypes()[0]->getBuiltinType();
}
}
42 changes: 42 additions & 0 deletions PropertyDescriber/DictionaryPropertyDescriber.php
@@ -0,0 +1,42 @@
<?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\PropertyDescriber;

use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;

final class DictionaryPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface, PropertyDescriberAwareInterface
{
use ModelRegistryAwareTrait;
use PropertyDescriberAwareTrait;

public function describe(array $types, OA\Schema $property, array $groups = null, ?OA\Schema $schema = null, array $context = [])
{
$property->type = 'object';
/** @var OA\AdditionalProperties $additionalProperties */
$additionalProperties = Util::getChild($property, OA\AdditionalProperties::class);

$this->propertyDescriber->describe($types[0]->getCollectionValueTypes(), $additionalProperties, $groups, $schema, $context);
}

/** {@inheritDoc} */
public function supports(array $types): bool
{
return 1 === count($types)
&& $types[0]->isCollection()
&& 1 === count($types[0]->getCollectionKeyTypes())
&& Type::BUILTIN_TYPE_STRING === $types[0]->getCollectionKeyTypes()[0]->getBuiltinType();
}
}
4 changes: 4 additions & 0 deletions Resources/config/services.xml
Expand Up @@ -110,6 +110,10 @@
<tag name="nelmio_api_doc.object_model.property_describer" priority="-1000" />
</service>

<service id="nelmio_api_doc.object_model.property_describers.dictionary" class="Nelmio\ApiDocBundle\PropertyDescriber\DictionaryPropertyDescriber" public="false">
<tag name="nelmio_api_doc.object_model.property_describer" priority="-1000" />
</service>

<service id="nelmio_api_doc.object_model.property_describers.boolean" class="Nelmio\ApiDocBundle\PropertyDescriber\BooleanPropertyDescriber" public="false">
<tag name="nelmio_api_doc.object_model.property_describer" priority="-1000" />
</service>
Expand Down
15 changes: 15 additions & 0 deletions Tests/Functional/Controller/ApiController80.php
Expand Up @@ -15,6 +15,7 @@
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItems\Dictionary;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItems\Foo;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArticleInterface;
Expand Down Expand Up @@ -558,4 +559,18 @@ public function nameConverterContext()
public function arbitraryArray()
{
}

/**
* @Route("/dictionary", methods={"GET"})
*
* @OA\Response(
* response=200,
* description="Success",
*
* @Model(type=Dictionary::class)
* )
*/
public function dictionary()
{
}
}
7 changes: 7 additions & 0 deletions Tests/Functional/Controller/ApiController81.php
Expand Up @@ -15,6 +15,7 @@
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItems\Dictionary;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItems\Foo;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81;
Expand Down Expand Up @@ -494,6 +495,12 @@ public function arbitraryArray()
{
}

#[Route('/dictionary', methods: ['GET'])]
#[OA\Response(response: 200, description: 'Success', content: new Model(type: Dictionary::class))]
public function dictionary()
{
}

#[Route('/article_map_query_string')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryString(
Expand Down
50 changes: 50 additions & 0 deletions Tests/Functional/Entity/ArrayItems/Dictionary.php
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\Tests\Functional\Entity\ArrayItems;

// PHP 7.2 is not able to guess these types
if (PHP_VERSION_ID < 70300) {
class Dictionary
{
}
} else {
class Dictionary
{
/**
* @var array<string, string>
*/
public $options;

/**
* @var array<string, string|integer>
*/
public $compoundOptions;

/**
* @var array<array<string, string|integer>>
*/
public $nestedCompoundOptions;

/**
* @var array<string, Foo>
*/
public $modelOptions;

/**
* @var array<int, string>
*/
public $listOptions;

/**
* @var array<int, string>|array<string, string>
*/
public $arrayOrDictOptions;

/**
* @var array<string, integer>
*/
public $integerOptions;
}
}
84 changes: 84 additions & 0 deletions Tests/Functional/FunctionalTest.php
Expand Up @@ -1233,6 +1233,90 @@ public function testArbitraryArrayModel()
], json_decode($this->getModel('Bar')->toJson(), true));
}

/**
* @requires PHP >= 7.3
*/
public function testDictionaryModel()
{
$this->getOperation('/api/dictionary', 'get');
self::assertEquals([
'schema' => 'Dictionary',
'required' => ['options', 'compoundOptions', 'nestedCompoundOptions', 'modelOptions', 'listOptions', 'arrayOrDictOptions', 'integerOptions'],
'properties' => [
'options' => [
'type' => 'object',
'additionalProperties' => [
'type' => 'string',
],
],
'compoundOptions' => [
'type' => 'object',
'additionalProperties' => [
'oneOf' => [
[
'type' => 'string',
],
[
'type' => 'integer',
],
],
],
],
'nestedCompoundOptions' => [
'type' => 'array',
'items' => [
'type' => 'object',
'additionalProperties' => [
'oneOf' => [
[
'type' => 'string',
],
[
'type' => 'integer',
],
],
],
],
],
'modelOptions' => [
'type' => 'object',
'additionalProperties' => [
'$ref' => '#/components/schemas/Foo',
],
],
'listOptions' => [
'type' => 'array',
'items' => [
'type' => 'string',
],
],
'arrayOrDictOptions' => [
'oneOf' => [
[
'type' => 'array',
'items' => [
'type' => 'string',
],
],
[
'type' => 'object',
'additionalProperties' => [
'type' => 'string',
],
],
],
],
'integerOptions' => [
'type' => 'object',
'additionalProperties' => [
'type' => 'integer',
],
],
],
'type' => 'object',
], json_decode($this->getModel('Dictionary')->toJson(), true));
}

public function testEntityWithFalsyDefaults()
{
$model = $this->getModel('EntityWithFalsyDefaults');
Expand Down
15 changes: 15 additions & 0 deletions phpunit-baseline.json
Expand Up @@ -7298,5 +7298,20 @@
"location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\SwaggerUiTest::testRedocly",
"message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2",
"count": 1
},
{
"location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testDictionaryModel",
"message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2",
"count": 1
},
{
"location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testDictionaryModel",
"message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2",
"count": 1
},
{
"location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testDictionaryModel",
"message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2",
"count": 1
}
]

0 comments on commit 61c08f1

Please sign in to comment.