Skip to content

Commit

Permalink
fix-2209: Multiple @OA\MediaType() warning with MapRequestPayload (#2213
Browse files Browse the repository at this point in the history
)

* refactor: TestKernel configureRoutes method

* improve testing to a per controller test

* uncomment commented code

* style fix

* more refactoring for new ControllerTest

* fix-2209: do not generate content if already exists

* style fix

* check if MapRequestPayload exists

* Add MapQueryStringController

* add test showcasing CleanUnusedComponents Processor

* style fix

* cleaner fix
  • Loading branch information
DjordyKoert authored Feb 7, 2024
1 parent 5a5049d commit daadb0b
Show file tree
Hide file tree
Showing 11 changed files with 6,853 additions and 19 deletions.
6 changes: 6 additions & 0 deletions Processor/MapRequestPayloadProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\SymfonyMapRequestPayloadDescriber;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use OpenApi\Processors\ProcessorInterface;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
Expand Down Expand Up @@ -55,7 +56,12 @@ public function __invoke(Analysis $analysis)
}

foreach ($formats as $format) {
if (!Generator::isDefault($requestBody->content)) {
continue;
}

$contentSchema = $this->getContentSchemaForType($requestBody, $format);

Util::modifyAnnotationValue($contentSchema, 'ref', $modelRef);

if ($argumentMetaData->isNullable()) {
Expand Down
4 changes: 4 additions & 0 deletions Tests/Functional/Configs/CleanUnusedComponentsProcessor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
OpenApi\Processors\CleanUnusedComponents:
tags:
- { name: 'nelmio_api_doc.swagger.processor', priority: -100 }
38 changes: 38 additions & 0 deletions Tests/Functional/ConfigurableContainerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\Tests\Functional;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Bundle\Bundle;

final class ConfigurableContainerFactory
{
/**
* @var ContainerInterface
*/
private $container;

/**
* @param Bundle[] $extraBundles
* @param string[] $extraConfigs
*/
public function create(array $extraBundles, ?callable $routeConfiguration, array $extraConfigs): void
{
// clear cache directory for a fresh container
$filesystem = new Filesystem();
$filesystem->remove('var/cache/test');

$appKernel = new NelmioKernel($extraBundles, $routeConfiguration, $extraConfigs);
$appKernel->boot();

$this->container = $appKernel->getContainer();
}

public function getContainer(): ContainerInterface
{
return $this->container;
}
}
25 changes: 25 additions & 0 deletions Tests/Functional/Controller/Controller2209.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;

use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Annotation\Route;

class Controller2209
{
#[Route(path: '/api/v3/users', name: 'api_v3_users_create', methods: 'POST')]
#[OA\RequestBody(
content: new OA\MediaType(mediaType: 'application/json', schema: new OA\Schema(
ref: new Model(type: Article81::class),
)),
)]
public function __invoke(#[MapRequestPayload] Article81 $requestDTO): JsonResponse
{
}
}
91 changes: 91 additions & 0 deletions Tests/Functional/Controller/MapQueryStringController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;

use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\ArrayQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\FilterQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\PaginationQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\QueryModel\SortQueryModel;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyMapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\Routing\Annotation\Route;

class MapQueryStringController
{
#[Route('/article_map_query_string')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryString(
#[MapQueryString] SymfonyMapQueryString $article81Query
) {
}

#[Route('/article_map_query_string_nullable')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryStringNullable(
#[MapQueryString] ?SymfonyMapQueryString $article81Query
) {
}

#[Route('/article_map_query_string_passes_validation_groups')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryStringHandlesValidationGroups(
#[MapQueryString(validationGroups: ['test'])] SymfonyConstraintsWithValidationGroups $symfonyConstraintsWithValidationGroups,
) {
}

#[Route('/article_map_query_string_overwrite_parameters')]
#[OA\Parameter(
name: 'id',
in: 'query',
schema: new OA\Schema(type: 'string', nullable: true),
description: 'Query parameter id description'
)]
#[OA\Parameter(
name: 'name',
in: 'query',
description: 'Query parameter name description'
)]
#[OA\Parameter(
name: 'nullableName',
in: 'query',
description: 'Query parameter nullableName description'
)]
#[OA\Parameter(
name: 'articleType81',
in: 'query',
description: 'Query parameter articleType81 description'
)]
#[OA\Parameter(
name: 'nullableArticleType81',
in: 'query',
description: 'Query parameter nullableArticleType81 description'
)]
#[OA\Response(response: '200', description: '')]
public function fetchArticleFromMapQueryStringOverwriteParameters(
#[MapQueryString] SymfonyMapQueryString $article81Query
) {
}

#[Route('/article_map_query_string_many_parameters')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleWithManyParameters(
#[MapQueryString] FilterQueryModel $filterQuery,
#[MapQueryString] PaginationQueryModel $paginationQuery,
#[MapQueryString] SortQueryModel $sortQuery,
#[MapQueryString] ArrayQueryModel $arrayQuery,
) {
}

#[Route('/article_map_query_string_many_parameters_optional')]
#[OA\Response(response: '200', description: '')]
public function fetchArticleWithManyOptionalParameters(
#[MapQueryString] ?FilterQueryModel $filterQuery,
#[MapQueryString] ?PaginationQueryModel $paginationQuery,
#[MapQueryString] ?SortQueryModel $sortQuery,
#[MapQueryString] ?ArrayQueryModel $arrayQuery,
) {
}
}
119 changes: 119 additions & 0 deletions Tests/Functional/ControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?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;

use OpenApi\Annotations as OA;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

/**
* Fairly intensive functional tests because the Kernel is recreated for each test.
*/
final class ControllerTest extends WebTestCase
{
/**
* @var ConfigurableContainerFactory
*/
private $configurableContainerFactory;

/**
* @var string[]
*/
private static $usedFixtures = [];

protected function setUp(): void
{
$this->configurableContainerFactory = new ConfigurableContainerFactory();

static::createClient([], ['HTTP_HOST' => 'api.example.com']);
}

protected static function createKernel(array $options = []): KernelInterface
{
return new NelmioKernel([], null, []);
}

protected function getOpenApiDefinition($area = 'default'): OA\OpenApi
{
return $this->configurableContainerFactory->getContainer()->get(sprintf('nelmio_api_doc.generator.%s', $area))->generate();
}

/**
* @dataProvider provideControllers
*/
public function testControllers(string $controllerName, ?string $fixtureName = null, array $extraConfigs = []): void
{
$fixtureName = $fixtureName ?? $controllerName;

$routingConfiguration = function (RoutingConfigurator $routes) use ($controllerName) {
$routes->withPath('/')->import(__DIR__."/Controller/$controllerName.php", 'attribute');
};

$this->configurableContainerFactory->create([], $routingConfiguration, $extraConfigs);

$apiDefinition = $this->getOpenApiDefinition();

// Create the fixture if it does not exist
if (!file_exists($fixtureDir = __DIR__.'/Fixtures/'.$fixtureName.'.json')) {
file_put_contents($fixtureDir, $apiDefinition->toJson());
}

static::$usedFixtures[] = $fixtureName.'.json';

self::assertSame(
self::getFixture($fixtureDir),
$this->getOpenApiDefinition()->toJson()
);
}

public static function provideControllers(): iterable
{
if (version_compare(Kernel::VERSION, '6.3.0', '>=')) {
yield 'https://github.com/nelmio/NelmioApiDocBundle/issues/2209' => ['Controller2209'];
yield 'MapQueryString' => ['MapQueryStringController'];
yield 'https://github.com/nelmio/NelmioApiDocBundle/issues/2191' => [
'MapQueryStringController',
'MapQueryStringCleanupComponents',
[__DIR__.'/Configs/CleanUnusedComponentsProcessor.yaml'],
];
}
}

/**
* @depends testControllers
*/
public function testUnusedFixtures(): void
{
$fixtures = glob(__DIR__.'/Fixtures/*.json');
$fixtures = array_map('basename', $fixtures);

$diff = array_diff($fixtures, static::$usedFixtures);

self::assertEmpty($diff, sprintf('The following fixtures are not used: %s', implode(', ', $diff)));
}

private static function getFixture(string $fixture): string
{
if (!file_exists($fixture)) {
self::fail(sprintf('The fixture file "%s" does not exist.', $fixture));
}

$content = file_get_contents($fixture);

if (false === $content) {
self::fail(sprintf('Failed to read the fixture file "%s".', $fixture));
}

return $content;
}
}
89 changes: 89 additions & 0 deletions Tests/Functional/Fixtures/Controller2209.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"openapi": "3.0.0",
"info": {
"title": "",
"version": "0.0.0"
},
"paths": {
"/api/v3/users": {
"post": {
"operationId": "post_api_v3_users_create",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Article81"
}
}
}
},
"responses": {
"default": {
"description": ""
}
}
}
}
},
"components": {
"schemas": {
"Article81": {
"required": [
"id",
"type",
"intBackedType",
"notBackedType"
],
"properties": {
"id": {
"type": "integer"
},
"type": {
"$ref": "#/components/schemas/ArticleType81"
},
"intBackedType": {
"$ref": "#/components/schemas/ArticleType81IntBacked"
},
"notBackedType": {
"$ref": "#/components/schemas/ArticleType81NotBacked"
},
"nullableType": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/ArticleType81"
}
]
}
},
"type": "object"
},
"ArticleType81": {
"type": "string",
"enum": [
"draft",
"final"
]
},
"ArticleType81IntBacked": {
"type": "integer",
"enum": [
0,
1
]
},
"ArticleType81NotBacked": {
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
},
"type": "object"
}
}
}
}
Loading

0 comments on commit daadb0b

Please sign in to comment.