Skip to content

Commit

Permalink
feat: create top level Tag from Tag annotations (#2334)
Browse files Browse the repository at this point in the history
Close #2333

| Q | A |

|---------------|---------------------------------------------------------------------------------------------------------------------------|
| Bug fix? | no |
| New feature? | yes <!-- please update src/**/CHANGELOG.md files --> |
| Deprecations? | no <!-- please update UPGRADE-*.md and
src/**/CHANGELOG.md files --> |
| Issues | Fix #2333 <!-- prefix each issue number with "Fix #", no need
to create an issue if none exists, explain below instead --> |

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 <[email protected]>
  • Loading branch information
noemi-salaun and Noémi Salaün authored Sep 3, 2024
1 parent 61a3f8b commit 4e66705
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Describer/OpenApiPhpDescriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
24 changes: 24 additions & 0 deletions src/OpenApiPhp/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions tests/Functional/Controller/OpenApiTagController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/*
* 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 OpenApi\Attributes as OA;
use Symfony\Component\Routing\Annotation\Route;

#[OA\Tag(name: 'My tag name', description: 'My description of the tag', externalDocs: new OA\ExternalDocumentation(url: 'https://example.com'))]
class OpenApiTagController
{
#[Route('/some_post', methods: ['POST'])]
#[OA\Response(response: '200', description: '')]
public function somePost()
{
}

#[Route('/some_get', methods: ['GET'])]
#[OA\Response(response: '200', description: '')]
public function someGet()
{
}
}
7 changes: 7 additions & 0 deletions tests/Functional/ControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ public static function provideAttributeTestCases(): \Generator
],
];

yield 'Create top level Tag from Tag attribute' => [
[
'name' => 'OpenApiTagController',
'type' => $type,
],
];

if (property_exists(MapRequestPayload::class, 'type')) {
yield 'Symfony 7.1 MapRequestPayload array type' => [
[
Expand Down
44 changes: 44 additions & 0 deletions tests/Functional/Fixtures/OpenApiTagController.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
18 changes: 18 additions & 0 deletions tests/SwaggerPhp/UtilTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down

0 comments on commit 4e66705

Please sign in to comment.