From 61c08f139e104db21b12832173b08fa37c22e619 Mon Sep 17 00:00:00 2001 From: Djordy Koert Date: Tue, 13 Feb 2024 15:15:29 +0100 Subject: [PATCH] feat-2207: Implement property describer for dictionary (#2208) * 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 --- CHANGELOG.md | 5 ++ PropertyDescriber/ArrayPropertyDescriber.php | 14 +++- .../DictionaryPropertyDescriber.php | 42 ++++++++++ Resources/config/services.xml | 4 + .../Functional/Controller/ApiController80.php | 15 ++++ .../Functional/Controller/ApiController81.php | 7 ++ .../Entity/ArrayItems/Dictionary.php | 50 +++++++++++ Tests/Functional/FunctionalTest.php | 84 +++++++++++++++++++ phpunit-baseline.json | 15 ++++ 9 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 PropertyDescriber/DictionaryPropertyDescriber.php create mode 100644 Tests/Functional/Entity/ArrayItems/Dictionary.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ac47c863c..6c7861c3e 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/PropertyDescriber/ArrayPropertyDescriber.php b/PropertyDescriber/ArrayPropertyDescriber.php index 7ff912174..7f6f76395 100644 --- a/PropertyDescriber/ArrayPropertyDescriber.php +++ b/PropertyDescriber/ArrayPropertyDescriber.php @@ -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 { @@ -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) { @@ -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(); } } diff --git a/PropertyDescriber/DictionaryPropertyDescriber.php b/PropertyDescriber/DictionaryPropertyDescriber.php new file mode 100644 index 000000000..95777bc10 --- /dev/null +++ b/PropertyDescriber/DictionaryPropertyDescriber.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 18cbdccca..469e87de9 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -110,6 +110,10 @@ + + + + diff --git a/Tests/Functional/Controller/ApiController80.php b/Tests/Functional/Controller/ApiController80.php index b81381541..184cb1055 100644 --- a/Tests/Functional/Controller/ApiController80.php +++ b/Tests/Functional/Controller/ApiController80.php @@ -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; @@ -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() + { + } } diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index f3bbe6861..0658ad6fe 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -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; @@ -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( diff --git a/Tests/Functional/Entity/ArrayItems/Dictionary.php b/Tests/Functional/Entity/ArrayItems/Dictionary.php new file mode 100644 index 000000000..23c992573 --- /dev/null +++ b/Tests/Functional/Entity/ArrayItems/Dictionary.php @@ -0,0 +1,50 @@ + + */ + public $options; + + /** + * @var array + */ + public $compoundOptions; + + /** + * @var array> + */ + public $nestedCompoundOptions; + + /** + * @var array + */ + public $modelOptions; + + /** + * @var array + */ + public $listOptions; + + /** + * @var array|array + */ + public $arrayOrDictOptions; + + /** + * @var array + */ + public $integerOptions; + } +} diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 702af37a5..f7f45a8e8 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -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'); diff --git a/phpunit-baseline.json b/phpunit-baseline.json index 01abb1955..b421f9eeb 100644 --- a/phpunit-baseline.json +++ b/phpunit-baseline.json @@ -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 } ]