From 4e667050c8b8248bd680e3ca06c573c730937605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mi=20Sala=C3=BCn?= Date: Tue, 3 Sep 2024 09:59:35 +0200 Subject: [PATCH] feat: create top level Tag from Tag annotations (#2334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close #2333 | Q | A | |---------------|---------------------------------------------------------------------------------------------------------------------------| | Bug fix? | no | | New feature? | yes | | Deprecations? | no | | Issues | Fix #2333 | When processing `OA\Tag` annotation inside the `OpenApiPhpDescriber`, the corresponding top level OpenApi Tag is created on the fly, so the description and the externalDocs are not lost. If the same Tag name is encountered multiple time, it is merged into the already existing top level tag. _There are 3 failing tests and 2 phpstan errors that are not related to my change. They were already there before my changes._ Co-authored-by: Noémi Salaün --- CHANGELOG.md | 4 ++ src/Describer/OpenApiPhpDescriber.php | 3 ++ src/OpenApiPhp/Util.php | 24 ++++++++++ .../Controller/OpenApiTagController.php | 33 ++++++++++++++ tests/Functional/ControllerTest.php | 7 +++ .../Fixtures/OpenApiTagController.json | 44 +++++++++++++++++++ tests/SwaggerPhp/UtilTest.php | 18 ++++++++ 7 files changed, 133 insertions(+) create mode 100644 tests/Functional/Controller/OpenApiTagController.php create mode 100644 tests/Functional/Fixtures/OpenApiTagController.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d46035549..5ea75a0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +next +----- +* Create top level OpenApi Tag from Tags top level annotations/attributes + 4.26.0 ----- * Add ability to configure UI through configuration diff --git a/src/Describer/OpenApiPhpDescriber.php b/src/Describer/OpenApiPhpDescriber.php index 14b792938..20cc958de 100644 --- a/src/Describer/OpenApiPhpDescriber.php +++ b/src/Describer/OpenApiPhpDescriber.php @@ -135,6 +135,9 @@ public function describe(OA\OpenApi $api): void $annotation->validate(); $mergeProperties->tags[] = $annotation->name; + $tag = Util::getTag($api, $annotation->name); + $tag->mergeProperties($annotation); + continue; } diff --git a/src/OpenApiPhp/Util.php b/src/OpenApiPhp/Util.php index 5dca4db7d..75d50ac2b 100644 --- a/src/OpenApiPhp/Util.php +++ b/src/OpenApiPhp/Util.php @@ -70,6 +70,30 @@ public static function getPath(OA\OpenApi $api, string $path): OA\PathItem return self::getIndexedCollectionItem($api, OA\PathItem::class, $path); } + /** + * Return an existing Tag object from $api->tags[] having its member name set to $name. + * Create, add to $api->tags[] and return this new Tag object and set the property if none found. + * + * @see OA\OpenApi::$tags + * @see OA\Tag::$name + */ + public static function getTag(OA\OpenApi $api, string $name): OA\Tag + { + // Tags ar not considered indexed, so we cannot use getIndexedCollectionItem directly + // because we need to specify that the search should use the "name" property. + $key = self::searchIndexedCollectionItem( + is_array($api->tags) ? $api->tags : [], + 'name', + $name + ); + + if (false === $key) { + $key = self::createCollectionItem($api, 'tags', OA\Tag::class, ['name' => $name]); + } + + return $api->tags[$key]; + } + /** * Return an existing Schema object from $api->components->schemas[] having its member schema set to $schema. * Create, add to $api->components->schemas[] and return this new Schema object and set the property if none found. diff --git a/tests/Functional/Controller/OpenApiTagController.php b/tests/Functional/Controller/OpenApiTagController.php new file mode 100644 index 000000000..735ca3fd3 --- /dev/null +++ b/tests/Functional/Controller/OpenApiTagController.php @@ -0,0 +1,33 @@ + [ + [ + 'name' => 'OpenApiTagController', + 'type' => $type, + ], + ]; + if (property_exists(MapRequestPayload::class, 'type')) { yield 'Symfony 7.1 MapRequestPayload array type' => [ [ diff --git a/tests/Functional/Fixtures/OpenApiTagController.json b/tests/Functional/Fixtures/OpenApiTagController.json new file mode 100644 index 000000000..ecd5c3f6b --- /dev/null +++ b/tests/Functional/Fixtures/OpenApiTagController.json @@ -0,0 +1,44 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "", + "version": "0.0.0" + }, + "paths": { + "/some_post": { + "post": { + "tags": [ + "My tag name" + ], + "operationId": "post_nelmio_apidoc_tests_functional_openapitag_somepost", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/some_get": { + "get": { + "tags": [ + "My tag name" + ], + "operationId": "get_nelmio_apidoc_tests_functional_openapitag_someget", + "responses": { + "200": { + "description": "" + } + } + } + } + }, + "tags": [ + { + "name": "My tag name", + "description": "My description of the tag", + "externalDocs": { + "url": "https://example.com" + } + } + ] +} \ No newline at end of file diff --git a/tests/SwaggerPhp/UtilTest.php b/tests/SwaggerPhp/UtilTest.php index f45699059..2510b2664 100644 --- a/tests/SwaggerPhp/UtilTest.php +++ b/tests/SwaggerPhp/UtilTest.php @@ -866,6 +866,24 @@ public static function provideMergeData(): \Generator ]; } + public function testGetTag(): void + { + $api = self::createObj(OA\OpenApi::class, ['_context' => new Context()]); + self::assertEquals(Generator::UNDEFINED, $api->tags); + + $tag = Util::getTag($api, 'foo'); + self::assertEquals('foo', $tag->name); + self::assertEquals(Generator::UNDEFINED, $tag->description); + self::assertEquals(Generator::UNDEFINED, $tag->externalDocs); + + self::assertIsArray($api->tags); + + $api->tags[] = self::createObj(OA\Tag::class, ['name' => 'bar', 'description' => 'baz']); + $tag = Util::getTag($api, 'bar'); + self::assertEquals('bar', $tag->name); + self::assertEquals('baz', $tag->description); + } + public function assertIsNested(OA\AbstractAnnotation $parent, OA\AbstractAnnotation $child): void { self::assertTrue($child->_context->is('nested'));