Extract rendering docs from command and controller

This commit is contained in:
Zdeněk Drahoš 2021-06-27 08:41:33 +02:00
parent 69d07979d3
commit 4ebee933c4
9 changed files with 305 additions and 66 deletions

View File

@ -11,9 +11,8 @@
namespace Nelmio\ApiDocBundle\Command; namespace Nelmio\ApiDocBundle\Command;
use Psr\Container\ContainerInterface; use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -21,16 +20,13 @@ use Symfony\Component\Console\Output\OutputInterface;
class DumpCommand extends Command class DumpCommand extends Command
{ {
/** /**
* @var ContainerInterface * @var RenderOpenApi
*/ */
private $generatorLocator; private $renderOpenApi;
/** public function __construct(RenderOpenApi $renderOpenApi)
* DumpCommand constructor.
*/
public function __construct(ContainerInterface $generatorLocator)
{ {
$this->generatorLocator = $generatorLocator; $this->renderOpenApi = $renderOpenApi;
parent::__construct(); parent::__construct();
} }
@ -48,25 +44,17 @@ class DumpCommand extends Command
} }
/** /**
* @throws InvalidArgumentException If the area to dump is not valid
*
* @return int|void * @return int|void
*/ */
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$area = $input->getOption('area'); $area = $input->getOption('area');
if (!$this->generatorLocator->has($area)) { $options = [
throw new InvalidArgumentException(sprintf('Area "%s" is not supported.', $area)); 'no-pretty' => $input->hasParameterOption(['--no-pretty']),
} ];
$docs = $this->renderOpenApi->render(RenderOpenApi::JSON, $area, $options);
$spec = $this->generatorLocator->get($area)->generate(); $output->writeln($docs, OutputInterface::OUTPUT_RAW);
if ($input->hasParameterOption(['--no-pretty'])) {
$output->writeln(json_encode($spec));
} else {
$output->writeln(json_encode($spec, JSON_PRETTY_PRINT));
}
return 0; return 0;
} }

View File

@ -11,33 +11,37 @@
namespace Nelmio\ApiDocBundle\Controller; namespace Nelmio\ApiDocBundle\Controller;
use OpenApi\Annotations\OpenApi; use InvalidArgumentException;
use OpenApi\Annotations\Server; use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Twig\Environment;
final class SwaggerUiController final class SwaggerUiController
{ {
private $generatorLocator; /**
* @var RenderOpenApi
*/
private $renderOpenApi;
private $twig; public function __construct(RenderOpenApi $renderOpenApi)
public function __construct(ContainerInterface $generatorLocator, $twig)
{ {
if (!$twig instanceof \Twig_Environment && !$twig instanceof Environment) { $this->renderOpenApi = $renderOpenApi;
throw new \InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', get_class($twig)));
}
$this->generatorLocator = $generatorLocator;
$this->twig = $twig;
} }
public function __invoke(Request $request, $area = 'default') public function __invoke(Request $request, $area = 'default')
{ {
if (!$this->generatorLocator->has($area)) { try {
$response = new Response(
$this->renderOpenApi->render(RenderOpenApi::HTML, $area, [
'server_url' => '' !== $request->getBaseUrl() ? $request->getSchemeAndHttpHost().$request->getBaseUrl() : null,
]),
Response::HTTP_OK,
['Content-Type' => 'text/html']
);
return $response->setCharset('UTF-8');
} catch (InvalidArgumentException $e) {
$advice = ''; $advice = '';
if (false !== strpos($area, '.json')) { if (false !== strpos($area, '.json')) {
$advice = ' Since the area provided contains `.json`, the issue is likely caused by route priorities. Try switching the Swagger UI / the json documentation routes order.'; $advice = ' Since the area provided contains `.json`, the issue is likely caused by route priorities. Try switching the Swagger UI / the json documentation routes order.';
@ -45,23 +49,5 @@ final class SwaggerUiController
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice)); throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice));
} }
/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();
if ('' !== $request->getBaseUrl()) {
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}
return new Response(
$this->twig->render(
'@NelmioApiDoc/SwaggerUi/index.html.twig',
['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]]
),
Response::HTTP_OK,
['Content-Type' => 'text/html']
);
return $response->setCharset('UTF-8');
} }
} }

View File

@ -0,0 +1,56 @@
<?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\Render\Html;
use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\OpenApiRenderer;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Twig\Environment;
class HtmlOpenApiRenderer implements OpenApiRenderer
{
/**
* @var Environment|\Twig_Environment
*/
private $twig;
public function __construct($twig)
{
if (!$twig instanceof \Twig_Environment && !$twig instanceof Environment) {
throw new InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', get_class($twig)));
}
$this->twig = $twig;
}
public function getFormat(): string
{
return RenderOpenApi::HTML;
}
public function render(OpenApi $spec, array $options = []): string
{
$options += [
'server_url' => null,
];
if ($options['server_url']) {
$spec->servers = [new Server(['url' => $options['server_url']])];
}
return $this->twig->render(
'@NelmioApiDoc/SwaggerUi/index.html.twig',
['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]]
);
}
}

View File

@ -0,0 +1,34 @@
<?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\Render\Json;
use Nelmio\ApiDocBundle\Render\OpenApiRenderer;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use OpenApi\Annotations\OpenApi;
class JsonOpenApiRenderer implements OpenApiRenderer
{
public function getFormat(): string
{
return RenderOpenApi::JSON;
}
public function render(OpenApi $spec, array $options = []): string
{
$options += [
'no-pretty' => false,
];
$flags = $options['no-pretty'] ? 0 : JSON_PRETTY_PRINT;
return json_encode($spec, $flags);
}
}

View File

@ -0,0 +1,21 @@
<?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\Render;
use OpenApi\Annotations\OpenApi;
interface OpenApiRenderer
{
public function getFormat(): string;
public function render(OpenApi $spec, array $options = []): string;
}

53
Render/RenderOpenApi.php Normal file
View File

@ -0,0 +1,53 @@
<?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\Render;
use InvalidArgumentException;
use OpenApi\Annotations\OpenApi;
use Psr\Container\ContainerInterface;
class RenderOpenApi
{
public const HTML = 'html';
public const JSON = 'json';
/** @var ContainerInterface */
private $generatorLocator;
/** @var array<string, OpenApiRenderer> */
private $openApiRenderers = [];
public function __construct(ContainerInterface $generatorLocator, OpenApiRenderer ...$openApiRenderers)
{
$this->generatorLocator = $generatorLocator;
foreach ($openApiRenderers as $openApiRenderer) {
$this->openApiRenderers[$openApiRenderer->getFormat()] = $openApiRenderer;
}
}
/**
* @throws InvalidArgumentException If the area to dump is not valid
*/
public function render(string $format, string $area, array $options = []): string
{
if (!$this->generatorLocator->has($area)) {
throw new InvalidArgumentException(sprintf('Area "%s" is not supported.', $area));
} elseif (!array_key_exists($format, $this->openApiRenderers)) {
throw new InvalidArgumentException(sprintf('Format "%s" is not supported.', $format));
}
/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();
return $this->openApiRenderers[$format]->render($spec, $options);
}
}

View File

@ -6,14 +6,13 @@
<services> <services>
<!-- Commands --> <!-- Commands -->
<service id="nelmio_api_doc.command.dump" class="Nelmio\ApiDocBundle\Command\DumpCommand" public="true"> <service id="nelmio_api_doc.command.dump" class="Nelmio\ApiDocBundle\Command\DumpCommand" public="true">
<argument type="service" id="nelmio_api_doc.generator_locator" /> <argument type="service" id="nelmio_api_doc.render_docs" />
<tag name="console.command" command="nelmio:apidoc:dump" /> <tag name="console.command" command="nelmio:apidoc:dump" />
</service> </service>
<!-- Controllers --> <!-- Controllers -->
<service id="nelmio_api_doc.controller.swagger_ui" class="Nelmio\ApiDocBundle\Controller\SwaggerUiController" public="true"> <service id="nelmio_api_doc.controller.swagger_ui" class="Nelmio\ApiDocBundle\Controller\SwaggerUiController" public="true">
<argument type="service" id="nelmio_api_doc.generator_locator" /> <argument type="service" id="nelmio_api_doc.render_docs" />
<argument type="service" id="twig" />
</service> </service>
<service id="nelmio_api_doc.controller.swagger" alias="nelmio_api_doc.controller.swagger_json" public="true" /> <service id="nelmio_api_doc.controller.swagger" alias="nelmio_api_doc.controller.swagger_json" public="true" />
@ -26,6 +25,18 @@
<argument type="service" id="nelmio_api_doc.generator_locator" /> <argument type="service" id="nelmio_api_doc.generator_locator" />
</service> </service>
<!-- Render -->
<service id="nelmio_api_doc.render_docs" class="Nelmio\ApiDocBundle\Render\RenderOpenApi" public="true">
<argument type="service" id="nelmio_api_doc.generator_locator" />
<argument type="service" id="nelmio_api_doc.render_docs.html" />
<argument type="service" id="nelmio_api_doc.render_docs.json" />
</service>
<service id="nelmio_api_doc.render_docs.html" class="Nelmio\ApiDocBundle\Render\Html\HtmlOpenApiRenderer" public="false">
<argument type="service" id="twig" />
</service>
<service id="nelmio_api_doc.render_docs.json" class="Nelmio\ApiDocBundle\Render\Json\JsonOpenApiRenderer" public="false">
</service>
<!-- Swagger Spec Generator --> <!-- Swagger Spec Generator -->
<service id="nelmio_api_doc.generator" alias="nelmio_api_doc.generator.default" public="true" /> <service id="nelmio_api_doc.generator" alias="nelmio_api_doc.generator.default" public="true" />

View File

@ -17,20 +17,35 @@ use Symfony\Component\Console\Tester\CommandTester;
class DumpCommandTest extends WebTestCase class DumpCommandTest extends WebTestCase
{ {
public function testExecute() /** @dataProvider provideJsonMode */
public function testJson(array $jsonOptions, int $expectedJsonFlags)
{
$output = $this->executeDumpCommand($jsonOptions + [
'--area' => 'test',
]);
$this->assertEquals(
json_encode($this->getOpenApiDefinition('test'), $expectedJsonFlags)."\n",
$output
);
}
public function provideJsonMode()
{
return [
'pretty print' => [[], JSON_PRETTY_PRINT],
'one line' => [['--no-pretty'], 0],
];
}
private function executeDumpCommand(array $options)
{ {
$kernel = static::bootKernel(); $kernel = static::bootKernel();
$application = new Application($kernel); $application = new Application($kernel);
$command = $application->find('nelmio:apidoc:dump'); $command = $application->find('nelmio:apidoc:dump');
$commandTester = new CommandTester($command); $commandTester = new CommandTester($command);
$commandTester->execute([ $commandTester->execute($options);
'--area' => 'test',
'--no-pretty' => '',
]);
// the output of the command in the console return $commandTester->getDisplay();
$output = $commandTester->getDisplay();
$this->assertEquals(json_encode($this->getOpenApiDefinition('test'))."\n", $output);
} }
} }

View File

@ -0,0 +1,75 @@
<?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\Render;
use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\OpenApiRenderer;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use OpenApi\Annotations\OpenApi;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
class RenderOpenApiTest extends TestCase
{
private $area = 'irrelevant area';
private $format = 'irrelevant format';
private $hasArea = true;
public function testRender()
{
$openApiRenderer = $this->createMock(OpenApiRenderer::class);
$openApiRenderer->method('getFormat')->willReturn($this->format);
$openApiRenderer->expects($this->once())->method('render');
$this->renderOpenApi($openApiRenderer);
}
public function testUnknownFormat()
{
$availableOpenApiRenderers = [];
$this->expectException(InvalidArgumentException::class);
$this->expectErrorMessage(sprintf('Format "%s" is not supported.', $this->format));
$this->renderOpenApi(...$availableOpenApiRenderers);
}
public function testUnknownArea()
{
$this->hasArea = false;
$this->expectException(InvalidArgumentException::class);
$this->expectErrorMessage(sprintf('Area "%s" is not supported.', $this->area));
$this->renderOpenApi();
}
private function renderOpenApi(...$openApiRenderer): void
{
$spec = $this->createMock(OpenApi::class);
$generator = new class($spec) {
private $spec;
public function __construct($spec)
{
$this->spec = $spec;
}
public function generate()
{
return $this->spec;
}
};
$generatorLocator = $this->createMock(ContainerInterface::class);
$generatorLocator->method('has')->willReturn($this->hasArea);
$generatorLocator->method('get')->willReturn($generator);
$renderOpenApi = new RenderOpenApi($generatorLocator, ...$openApiRenderer);
$renderOpenApi->render($this->format, $this->area, []);
}
}