Enable dumping html docs with cdn and offline assets

This commit is contained in:
Zdeněk Drahoš 2021-06-27 09:24:35 +02:00
parent 4ebee933c4
commit 5124f07ece
11 changed files with 250 additions and 17 deletions

View File

@ -11,6 +11,7 @@
namespace Nelmio\ApiDocBundle\Command; namespace Nelmio\ApiDocBundle\Command;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -24,6 +25,15 @@ class DumpCommand extends Command
*/ */
private $renderOpenApi; private $renderOpenApi;
/**
* @var mixed[]
*/
private $defaultHtmlConfig = [
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
'server_url' => null,
];
public function __construct(RenderOpenApi $renderOpenApi) public function __construct(RenderOpenApi $renderOpenApi)
{ {
$this->renderOpenApi = $renderOpenApi; $this->renderOpenApi = $renderOpenApi;
@ -36,9 +46,18 @@ class DumpCommand extends Command
*/ */
protected function configure() protected function configure()
{ {
$availableFormats = $this->renderOpenApi->getAvailableFormats();
$this $this
->setDescription('Dumps documentation in OpenAPI JSON format') ->setDescription('Dumps documentation in OpenAPI JSON format or HTML')
->addOption('area', '', InputOption::VALUE_OPTIONAL, '', 'default') ->addOption('area', '', InputOption::VALUE_OPTIONAL, '', 'default')
->addOption(
'format',
'',
InputOption::VALUE_REQUIRED,
'Output format like: '.implode(', ', $availableFormats),
RenderOpenApi::JSON
)
->addOption('html-config', '', InputOption::VALUE_REQUIRED, '', json_encode($this->defaultHtmlConfig))
->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format output') ->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format output')
; ;
} }
@ -49,11 +68,19 @@ class DumpCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$area = $input->getOption('area'); $area = $input->getOption('area');
$format = $input->getOption('format');
$options = [ $options = [];
'no-pretty' => $input->hasParameterOption(['--no-pretty']), if (RenderOpenApi::HTML === $format) {
]; $rawHtmlConfig = json_decode($input->getOption('html-config'), true);
$docs = $this->renderOpenApi->render(RenderOpenApi::JSON, $area, $options); $options = is_array($rawHtmlConfig) ? $rawHtmlConfig : $this->defaultHtmlConfig;
} elseif (RenderOpenApi::JSON === $format) {
$options = [
'no-pretty' => $input->hasParameterOption(['--no-pretty']),
];
}
$docs = $this->renderOpenApi->render($format, $area, $options);
$output->writeln($docs, OutputInterface::OUTPUT_RAW); $output->writeln($docs, OutputInterface::OUTPUT_RAW);
return 0; return 0;

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Controller; namespace Nelmio\ApiDocBundle\Controller;
use InvalidArgumentException; use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -35,6 +36,7 @@ final class SwaggerUiController
$response = new Response( $response = new Response(
$this->renderOpenApi->render(RenderOpenApi::HTML, $area, [ $this->renderOpenApi->render(RenderOpenApi::HTML, $area, [
'server_url' => '' !== $request->getBaseUrl() ? $request->getSchemeAndHttpHost().$request->getBaseUrl() : null, 'server_url' => '' !== $request->getBaseUrl() ? $request->getSchemeAndHttpHost().$request->getBaseUrl() : null,
'assets_mode' => AssetsMode::BUNDLE,
]), ]),
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']

View File

@ -0,0 +1,19 @@
<?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;
class AssetsMode
{
public const BUNDLE = 'bundle';
public const CDN = 'cdn';
public const OFFLINE = 'offline';
}

View File

@ -0,0 +1,41 @@
<?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 Symfony\Bridge\Twig\Extension\AssetExtension;
class GetNelmioAsset
{
private $assetExtension;
private $defaultAssetsMode;
public function __construct(AssetExtension $assetExtension, $defaultAssetsMode)
{
$this->assetExtension = $assetExtension;
$this->defaultAssetsMode = $defaultAssetsMode;
}
public function __invoke($asset, $forcedMode = null)
{
$mode = $forcedMode ?: $this->defaultAssetsMode;
if (AssetsMode::CDN === $mode) {
return sprintf(
'https://cdn.jsdelivr.net/gh/nelmio/NelmioApiDocBundle@4.1/Resources/public/%s',
$asset
);
} elseif (AssetsMode::OFFLINE === $mode) {
return file_get_contents(__DIR__.sprintf('/../../Resources/public/%s', $asset));
} else {
return $this->assetExtension->getAssetUrl(sprintf('bundles/nelmioapidoc/%s', $asset));
}
}
}

View File

@ -16,7 +16,9 @@ use Nelmio\ApiDocBundle\Render\OpenApiRenderer;
use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server; use OpenApi\Annotations\Server;
use Symfony\Bridge\Twig\Extension\AssetExtension;
use Twig\Environment; use Twig\Environment;
use Twig\TwigFunction;
class HtmlOpenApiRenderer implements OpenApiRenderer class HtmlOpenApiRenderer implements OpenApiRenderer
{ {
@ -24,13 +26,18 @@ class HtmlOpenApiRenderer implements OpenApiRenderer
* @var Environment|\Twig_Environment * @var Environment|\Twig_Environment
*/ */
private $twig; private $twig;
/**
* @var AssetExtension
*/
private $assetExtension;
public function __construct($twig) public function __construct($twig, AssetExtension $assetExtension)
{ {
if (!$twig instanceof \Twig_Environment && !$twig instanceof Environment) { 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))); throw new InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', get_class($twig)));
} }
$this->twig = $twig; $this->twig = $twig;
$this->assetExtension = $assetExtension;
} }
public function getFormat(): string public function getFormat(): string
@ -42,15 +49,33 @@ class HtmlOpenApiRenderer implements OpenApiRenderer
{ {
$options += [ $options += [
'server_url' => null, 'server_url' => null,
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
]; ];
if ($options['server_url']) { $this->twig->addFunction(
$spec->servers = [new Server(['url' => $options['server_url']])]; new TwigFunction(
} 'nelmioAsset',
new GetNelmioAsset($this->assetExtension, $options['assets_mode'])
)
);
return $this->twig->render( return $this->twig->render(
'@NelmioApiDoc/SwaggerUi/index.html.twig', '@NelmioApiDoc/SwaggerUi/index.html.twig',
['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]] [
'swagger_data' => ['spec' => $this->createJsonSpec($spec, $options['server_url'])],
'assets_mode' => $options['assets_mode'],
'swagger_ui_config' => $options['swagger_ui_config'],
]
); );
} }
private function createJsonSpec(OpenApi $spec, $serverUrl)
{
if ($serverUrl) {
$spec->servers = [new Server(['url' => $serverUrl])];
}
return json_decode($spec->toJson(), true);
}
} }

View File

@ -34,6 +34,11 @@ class RenderOpenApi
} }
} }
public function getAvailableFormats(): array
{
return array_keys($this->openApiRenderers);
}
/** /**
* @throws InvalidArgumentException If the area to dump is not valid * @throws InvalidArgumentException If the area to dump is not valid
*/ */

View File

@ -33,6 +33,7 @@
</service> </service>
<service id="nelmio_api_doc.render_docs.html" class="Nelmio\ApiDocBundle\Render\Html\HtmlOpenApiRenderer" public="false"> <service id="nelmio_api_doc.render_docs.html" class="Nelmio\ApiDocBundle\Render\Html\HtmlOpenApiRenderer" public="false">
<argument type="service" id="twig" /> <argument type="service" id="twig" />
<argument type="service" id="twig.extension.assets" />
</service> </service>
<service id="nelmio_api_doc.render_docs.json" class="Nelmio\ApiDocBundle\Render\Json\JsonOpenApiRenderer" public="false"> <service id="nelmio_api_doc.render_docs.json" class="Nelmio\ApiDocBundle\Render\Json\JsonOpenApiRenderer" public="false">
</service> </service>

View File

@ -0,0 +1,39 @@
Commands
========
A command is provided in order to dump the documentation in ``json`` or ``html``.
.. code-block:: bash
$ php app/console api:doc:dump [--format="..."]
The ``--format`` option allows to choose the format (default is: ``json``).
By default, the generated JSON will be pretty-formatted. If you want to generate a json
without whitespace, use the ``--no-pretty`` option.
.. code-block:: bash
$ php app/console api:doc:dump --format=json > json-pretty-formatted.json
$ php app/console api:doc:dump --format=json --no-pretty > json-no-pretty.json
For example to generate a static version of your documentation you can use:
.. code-block:: bash
$ php app/console api:doc:dump --format=html > api.html
By default, the generated HTML will add the sandbox feature.
If you want to generate a static version of your documentation without sandbox,
or configure UI configuration, use the ``--html-config`` option.
- ``assets_mode`` - `cdn` loads assets from CDN, `offline` inlines assets
- ``server_url`` - API url, useful if static documentation is not hosted on API url
- ``swagger_ui_config`` - `configure Swagger UI`_
- ``"supportedSubmitMethods":[]`` disables the sandbox
.. code-block:: bash
$ php app/console api:doc:dump --format=html --html-config '{"assets_mode":"offline","server_url":"https://example.com","swagger_ui_config":{"supportedSubmitMethods":[]}}' > api.html
.. _`configure Swagger UI`: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/

View File

@ -339,6 +339,7 @@ If you need more complex features, take a look at:
areas areas
alternative_names alternative_names
customization customization
commands
faq faq
.. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html .. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html

View File

@ -14,8 +14,13 @@ file that was distributed with this source code. #}
<title>{% block title %}{{ swagger_data.spec.info.title }}{% endblock title %}</title> <title>{% block title %}{{ swagger_data.spec.info.title }}{% endblock title %}</title>
{% block stylesheets %} {% block stylesheets %}
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui.css') }}"> {% if assets_mode == 'offline' %}
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/style.css') }}"> <style>{{ nelmioAsset('swagger-ui/swagger-ui.css')|raw }}</style>
<style>{{ nelmioAsset('style.css')|raw }}</style>
{% else %}
<link rel="stylesheet" href="{{ nelmioAsset('swagger-ui/swagger-ui.css') }}">
<link rel="stylesheet" href="{{ nelmioAsset('style.css.css') }}">
{% endif %}
{% endblock stylesheets %} {% endblock stylesheets %}
{% block swagger_data %} {% block swagger_data %}
@ -53,7 +58,13 @@ file that was distributed with this source code. #}
{% endblock svg_icons %} {% endblock svg_icons %}
<header> <header>
{% block header %} {% block header %}
<a id="logo" href="https://github.com/nelmio/NelmioApiDocBundle"><img src="{{ asset('bundles/nelmioapidoc/logo.png') }}" alt="NelmioApiDocBundle"></a> <a id="logo" href="https://github.com/nelmio/NelmioApiDocBundle">
{% if assets_mode in ['offline', 'cdn'] %}
<img src="{{ nelmioAsset('logo.png', 'cdn') }}" alt="NelmioApiDocBundle">
{% else %}
<img src="{{ nelmioAsset('logo.png') }}" alt="NelmioApiDocBundle">
{% endif %}
</a>
{% endblock header %} {% endblock header %}
</header> </header>
@ -62,14 +73,27 @@ file that was distributed with this source code. #}
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-bundle.js') }}"></script> {% if assets_mode == 'offline' %}
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-standalone-preset.js') }}"></script> <script>{{ nelmioAsset('swagger-ui/swagger-ui-bundle.js')|raw }}</script>
<script>{{ nelmioAsset('swagger-ui/swagger-ui-standalone-preset.js')|raw }}</script>
{% else %}
<script src="{{ nelmioAsset('swagger-ui/swagger-ui-bundle.js') }}"></script>
<script src="{{ nelmioAsset('swagger-ui/swagger-ui-standalone-preset.js') }}"></script>
{% endif %}
{% endblock javascripts %} {% endblock javascripts %}
<script src="{{ asset('bundles/nelmioapidoc/init-swagger-ui.js') }}"></script> {% if assets_mode == 'offline' %}
<script>{{ nelmioAsset('init-swagger-ui.js')|raw }}</script>
{% else %}
<script src="{{ nelmioAsset('init-swagger-ui.js') }}"></script>
{% endif %}
{% block swagger_initialization %} {% block swagger_initialization %}
<script type="text/javascript"> <script type="text/javascript">
window.onload = loadSwaggerUI(); (function () {
var swaggerUI = {{ swagger_ui_config|json_encode(65)|raw }};
window.onload = loadSwaggerUI(swaggerUI);
})();
</script> </script>
{% endblock swagger_initialization %} {% endblock swagger_initialization %}
</body> </body>

View File

@ -11,6 +11,7 @@
namespace Nelmio\ApiDocBundle\Tests\Command; namespace Nelmio\ApiDocBundle\Tests\Command;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase; // for the creation of the kernel use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase; // for the creation of the kernel
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
@ -37,6 +38,54 @@ class DumpCommandTest extends WebTestCase
]; ];
} }
/** @dataProvider provideAssetsMode */
public function testHtml($htmlConfig, string $expectedHtml)
{
$output = $this->executeDumpCommand([
'--area' => 'test',
'--format' => 'html',
'--html-config' => json_encode($htmlConfig),
]);
self::assertStringContainsString('<body>', $output);
self::assertStringContainsString($expectedHtml, $output);
}
public function provideAssetsMode()
{
return [
'default mode is cdn' => [
null,
'https://cdn.jsdelivr.net',
],
'invalid mode fallbacks to cdn' => [
'invalid',
'https://cdn.jsdelivr.net',
],
'select cdn mode' => [
['assets_mode' => AssetsMode::CDN],
'https://cdn.jsdelivr.net',
],
'select offline mode' => [
['assets_mode' => AssetsMode::OFFLINE],
'<style>',
],
'configure swagger ui' => [
[
'swagger_ui_config' => [
'supportedSubmitMethods' => ['get'],
],
],
'"supportedSubmitMethods":["get"]',
],
'configure server url' => [
[
'server_url' => 'http://example.com/api',
],
'[{"url":"http://example.com/api"}]',
],
];
}
private function executeDumpCommand(array $options) private function executeDumpCommand(array $options)
{ {
$kernel = static::bootKernel(); $kernel = static::bootKernel();