From 5124f07ece91d211c53730c4b689c1d20886b6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdene=CC=8Ck=20Drahos=CC=8C?= Date: Sun, 27 Jun 2021 09:24:35 +0200 Subject: [PATCH] Enable dumping html docs with cdn and offline assets --- Command/DumpCommand.php | 37 ++++++++++++++--- Controller/SwaggerUiController.php | 2 + Render/Html/AssetsMode.php | 19 +++++++++ Render/Html/GetNelmioAsset.php | 41 +++++++++++++++++++ Render/Html/HtmlOpenApiRenderer.php | 35 +++++++++++++--- Render/RenderOpenApi.php | 5 +++ Resources/config/services.xml | 1 + Resources/doc/commands.rst | 39 ++++++++++++++++++ Resources/doc/index.rst | 1 + Resources/views/SwaggerUi/index.html.twig | 38 ++++++++++++++---- Tests/Command/DumpCommandTest.php | 49 +++++++++++++++++++++++ 11 files changed, 250 insertions(+), 17 deletions(-) create mode 100644 Render/Html/AssetsMode.php create mode 100644 Render/Html/GetNelmioAsset.php create mode 100644 Resources/doc/commands.rst diff --git a/Command/DumpCommand.php b/Command/DumpCommand.php index b2438da..481c682 100644 --- a/Command/DumpCommand.php +++ b/Command/DumpCommand.php @@ -11,6 +11,7 @@ namespace Nelmio\ApiDocBundle\Command; +use Nelmio\ApiDocBundle\Render\Html\AssetsMode; use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -24,6 +25,15 @@ class DumpCommand extends Command */ private $renderOpenApi; + /** + * @var mixed[] + */ + private $defaultHtmlConfig = [ + 'assets_mode' => AssetsMode::CDN, + 'swagger_ui_config' => [], + 'server_url' => null, + ]; + public function __construct(RenderOpenApi $renderOpenApi) { $this->renderOpenApi = $renderOpenApi; @@ -36,9 +46,18 @@ class DumpCommand extends Command */ protected function configure() { + $availableFormats = $this->renderOpenApi->getAvailableFormats(); $this - ->setDescription('Dumps documentation in OpenAPI JSON format') + ->setDescription('Dumps documentation in OpenAPI JSON format or HTML') ->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') ; } @@ -49,11 +68,19 @@ class DumpCommand extends Command protected function execute(InputInterface $input, OutputInterface $output) { $area = $input->getOption('area'); + $format = $input->getOption('format'); - $options = [ - 'no-pretty' => $input->hasParameterOption(['--no-pretty']), - ]; - $docs = $this->renderOpenApi->render(RenderOpenApi::JSON, $area, $options); + $options = []; + if (RenderOpenApi::HTML === $format) { + $rawHtmlConfig = json_decode($input->getOption('html-config'), true); + $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); return 0; diff --git a/Controller/SwaggerUiController.php b/Controller/SwaggerUiController.php index 4edad7f..1d4cca4 100644 --- a/Controller/SwaggerUiController.php +++ b/Controller/SwaggerUiController.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Controller; use InvalidArgumentException; +use Nelmio\ApiDocBundle\Render\Html\AssetsMode; use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -35,6 +36,7 @@ final class SwaggerUiController $response = new Response( $this->renderOpenApi->render(RenderOpenApi::HTML, $area, [ 'server_url' => '' !== $request->getBaseUrl() ? $request->getSchemeAndHttpHost().$request->getBaseUrl() : null, + 'assets_mode' => AssetsMode::BUNDLE, ]), Response::HTTP_OK, ['Content-Type' => 'text/html'] diff --git a/Render/Html/AssetsMode.php b/Render/Html/AssetsMode.php new file mode 100644 index 0000000..2bbdf5a --- /dev/null +++ b/Render/Html/AssetsMode.php @@ -0,0 +1,19 @@ +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)); + } + } +} diff --git a/Render/Html/HtmlOpenApiRenderer.php b/Render/Html/HtmlOpenApiRenderer.php index 32417be..5bb8645 100644 --- a/Render/Html/HtmlOpenApiRenderer.php +++ b/Render/Html/HtmlOpenApiRenderer.php @@ -16,7 +16,9 @@ use Nelmio\ApiDocBundle\Render\OpenApiRenderer; use Nelmio\ApiDocBundle\Render\RenderOpenApi; use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\Server; +use Symfony\Bridge\Twig\Extension\AssetExtension; use Twig\Environment; +use Twig\TwigFunction; class HtmlOpenApiRenderer implements OpenApiRenderer { @@ -24,13 +26,18 @@ class HtmlOpenApiRenderer implements OpenApiRenderer * @var Environment|\Twig_Environment */ private $twig; + /** + * @var AssetExtension + */ + private $assetExtension; - public function __construct($twig) + public function __construct($twig, AssetExtension $assetExtension) { 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; + $this->assetExtension = $assetExtension; } public function getFormat(): string @@ -42,15 +49,33 @@ class HtmlOpenApiRenderer implements OpenApiRenderer { $options += [ 'server_url' => null, + 'assets_mode' => AssetsMode::CDN, + 'swagger_ui_config' => [], ]; - if ($options['server_url']) { - $spec->servers = [new Server(['url' => $options['server_url']])]; - } + $this->twig->addFunction( + new TwigFunction( + 'nelmioAsset', + new GetNelmioAsset($this->assetExtension, $options['assets_mode']) + ) + ); return $this->twig->render( '@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); + } } diff --git a/Render/RenderOpenApi.php b/Render/RenderOpenApi.php index aace14f..1407b67 100644 --- a/Render/RenderOpenApi.php +++ b/Render/RenderOpenApi.php @@ -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 */ diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 47174fd..e0127ae 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -33,6 +33,7 @@ + diff --git a/Resources/doc/commands.rst b/Resources/doc/commands.rst new file mode 100644 index 0000000..fe4df38 --- /dev/null +++ b/Resources/doc/commands.rst @@ -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/ diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index fdc7c0e..9daa3ea 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -339,6 +339,7 @@ If you need more complex features, take a look at: areas alternative_names customization + commands faq .. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html diff --git a/Resources/views/SwaggerUi/index.html.twig b/Resources/views/SwaggerUi/index.html.twig index 82e1332..0db7f8b 100644 --- a/Resources/views/SwaggerUi/index.html.twig +++ b/Resources/views/SwaggerUi/index.html.twig @@ -14,8 +14,13 @@ file that was distributed with this source code. #} {% block title %}{{ swagger_data.spec.info.title }}{% endblock title %} {% block stylesheets %} - - + {% if assets_mode == 'offline' %} + + + {% else %} + + + {% endif %} {% endblock stylesheets %} {% block swagger_data %} @@ -53,7 +58,13 @@ file that was distributed with this source code. #} {% endblock svg_icons %}
{% block header %} - + {% endblock header %}
@@ -62,14 +73,27 @@ file that was distributed with this source code. #} {% endblock %} {% block javascripts %} - - + {% if assets_mode == 'offline' %} + + + {% else %} + + + {% endif %} {% endblock javascripts %} - + {% if assets_mode == 'offline' %} + + {% else %} + + {% endif %} + {% block swagger_initialization %} {% endblock swagger_initialization %} diff --git a/Tests/Command/DumpCommandTest.php b/Tests/Command/DumpCommandTest.php index 76902d2..23e6f3a 100644 --- a/Tests/Command/DumpCommandTest.php +++ b/Tests/Command/DumpCommandTest.php @@ -11,6 +11,7 @@ namespace Nelmio\ApiDocBundle\Tests\Command; +use Nelmio\ApiDocBundle\Render\Html\AssetsMode; use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase; // for the creation of the kernel use Symfony\Bundle\FrameworkBundle\Console\Application; 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('', $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], + '