Merge pull request #1842 from zdenekdrahos/html-dump

Dump documentation to a static HTML and YAML file
This commit is contained in:
Guilhem Niot 2021-09-03 20:28:19 +02:00 committed by GitHub
commit b53edda10c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 790 additions and 105 deletions

View File

@ -11,9 +11,9 @@
namespace Nelmio\ApiDocBundle\Command;
use Psr\Container\ContainerInterface;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -21,16 +21,21 @@ use Symfony\Component\Console\Output\OutputInterface;
class DumpCommand extends Command
{
/**
* @var ContainerInterface
* @var RenderOpenApi
*/
private $generatorLocator;
private $renderOpenApi;
/**
* DumpCommand constructor.
* @var mixed[]
*/
public function __construct(ContainerInterface $generatorLocator)
private $defaultHtmlConfig = [
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
];
public function __construct(RenderOpenApi $renderOpenApi)
{
$this->generatorLocator = $generatorLocator;
$this->renderOpenApi = $renderOpenApi;
parent::__construct();
}
@ -40,34 +45,48 @@ class DumpCommand extends Command
*/
protected function configure()
{
$availableFormats = $this->renderOpenApi->getAvailableFormats();
$this
->setDescription('Dumps documentation in OpenAPI JSON format')
->setDescription('Dumps documentation in OpenAPI format to: '.implode(', ', $availableFormats))
->addOption('area', '', InputOption::VALUE_OPTIONAL, '', 'default')
->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format output')
->addOption(
'format',
'',
InputOption::VALUE_REQUIRED,
'Output format like: '.implode(', ', $availableFormats),
RenderOpenApi::JSON
)
->addOption('server-url', '', InputOption::VALUE_REQUIRED, 'URL where live api doc is served')
->addOption('html-config', '', InputOption::VALUE_REQUIRED, '', json_encode($this->defaultHtmlConfig))
->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format JSON output')
;
}
/**
* @throws InvalidArgumentException If the area to dump is not valid
*
* @return int|void
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$area = $input->getOption('area');
$format = $input->getOption('format');
if (!$this->generatorLocator->has($area)) {
throw new InvalidArgumentException(sprintf('Area "%s" is not supported.', $area));
$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']),
];
}
$spec = $this->generatorLocator->get($area)->generate();
if ($input->hasParameterOption(['--no-pretty'])) {
$output->writeln(json_encode($spec));
} else {
$output->writeln(json_encode($spec, JSON_PRETTY_PRINT));
if ($input->getOption('server-url')) {
$options['server_url'] = $input->getOption('server-url');
}
$docs = $this->renderOpenApi->render($format, $area, $options);
$output->writeln($docs, OutputInterface::OUTPUT_RAW);
return 0;
}
}

View File

@ -11,35 +11,31 @@
namespace Nelmio\ApiDocBundle\Controller;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
final class DocumentationController
{
private $generatorLocator;
/**
* @var RenderOpenApi
*/
private $renderOpenApi;
public function __construct(ContainerInterface $generatorLocator)
public function __construct(RenderOpenApi $renderOpenApi)
{
$this->generatorLocator = $generatorLocator;
$this->renderOpenApi = $renderOpenApi;
}
public function __invoke(Request $request, $area = 'default')
{
if (!$this->generatorLocator->has($area)) {
try {
return JsonResponse::fromJsonString(
$this->renderOpenApi->renderFromRequest($request, RenderOpenApi::JSON, $area)
);
} catch (InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
}
/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();
if ('' !== $request->getBaseUrl()) {
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}
return new JsonResponse($spec);
}
}

View File

@ -11,33 +11,38 @@
namespace Nelmio\ApiDocBundle\Controller;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Twig\Environment;
final class SwaggerUiController
{
private $generatorLocator;
/**
* @var RenderOpenApi
*/
private $renderOpenApi;
private $twig;
public function __construct(ContainerInterface $generatorLocator, $twig)
public function __construct(RenderOpenApi $renderOpenApi)
{
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->generatorLocator = $generatorLocator;
$this->twig = $twig;
$this->renderOpenApi = $renderOpenApi;
}
public function __invoke(Request $request, $area = 'default')
{
if (!$this->generatorLocator->has($area)) {
try {
$response = new Response(
$this->renderOpenApi->renderFromRequest($request, RenderOpenApi::HTML, $area, [
'assets_mode' => AssetsMode::BUNDLE,
]),
Response::HTTP_OK,
['Content-Type' => 'text/html']
);
return $response->setCharset('UTF-8');
} catch (InvalidArgumentException $e) {
$advice = '';
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.';
@ -45,23 +50,5 @@ final class SwaggerUiController
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

@ -11,37 +11,36 @@
namespace Nelmio\ApiDocBundle\Controller;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
final class YamlDocumentationController
{
private $generatorLocator;
/**
* @var RenderOpenApi
*/
private $renderOpenApi;
public function __construct(ContainerInterface $generatorLocator)
public function __construct(RenderOpenApi $renderOpenApi)
{
$this->generatorLocator = $generatorLocator;
$this->renderOpenApi = $renderOpenApi;
}
public function __invoke(Request $request, $area = 'default')
{
if (!$this->generatorLocator->has($area)) {
try {
$response = new Response(
$this->renderOpenApi->renderFromRequest($request, RenderOpenApi::YAML, $area),
Response::HTTP_OK,
['Content-Type' => 'text/x-yaml']
);
return $response->setCharset('UTF-8');
} catch (InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
}
/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();
if ('' !== $request->getBaseUrl()) {
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}
return new Response($spec->toYaml(), 200, [
'Content-Type' => 'text/x-yaml',
]);
}
}

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,96 @@
<?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;
use Twig\TwigFunction;
/**
* @internal
*/
class GetNelmioAsset
{
private $assetExtension;
private $resourcesDir;
private $cdnUrl;
private $assetsMode = AssetsMode::BUNDLE;
public function __construct(AssetExtension $assetExtension)
{
$this->assetExtension = $assetExtension;
$this->cdnUrl = 'https://cdn.jsdelivr.net/gh/nelmio/NelmioApiDocBundle/Resources/public';
$this->resourcesDir = __DIR__.'/../../Resources/public';
}
public function toTwigFunction($assetsMode): TwigFunction
{
$this->assetsMode = $assetsMode;
return new TwigFunction('nelmioAsset', $this, ['is_safe' => ['html']]);
}
public function __invoke($asset)
{
[$extension, $mode] = $this->getExtension($asset);
[$resource, $isInline] = $this->getResource($asset, $mode);
if ('js' == $extension) {
return $this->renderJavascript($resource, $isInline);
} elseif ('css' == $extension) {
return $this->renderCss($resource, $isInline);
} else {
return $resource;
}
}
private function getExtension($asset)
{
$extension = mb_substr($asset, -3, 3, 'utf-8');
if ('.js' === $extension) {
return ['js', $this->assetsMode];
} elseif ('png' === $extension) {
return ['png', AssetsMode::OFFLINE == $this->assetsMode ? AssetsMode::CDN : $this->assetsMode];
} else {
return ['css', $this->assetsMode];
}
}
private function getResource($asset, $mode)
{
if (filter_var($asset, FILTER_VALIDATE_URL)) {
return [$asset, false];
} elseif (AssetsMode::OFFLINE === $mode) {
return [file_get_contents($this->resourcesDir.'/'.$asset), true];
} elseif (AssetsMode::CDN === $mode) {
return [$this->cdnUrl.'/'.$asset, false];
} else {
return [$this->assetExtension->getAssetUrl(sprintf('bundles/nelmioapidoc/%s', $asset)), false];
}
}
private function renderJavascript(string $script, bool $isInline)
{
if ($isInline) {
return sprintf('<script>%s</script>', $script);
} else {
return sprintf('<script src="%s"></script>', $script);
}
}
private function renderCss(string $stylesheet, bool $isInline)
{
if ($isInline) {
return sprintf('<style>%s</style>', $stylesheet);
} else {
return sprintf('<link rel="stylesheet" href="%s">', $stylesheet);
}
}
}

View File

@ -0,0 +1,63 @@
<?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 Twig\Environment;
/**
* @internal
*/
class HtmlOpenApiRenderer implements OpenApiRenderer
{
/** @var Environment|\Twig_Environment */
private $twig;
/** @var GetNelmioAsset */
private $getNelmioAsset;
public function __construct($twig, GetNelmioAsset $getNelmioAsset)
{
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->getNelmioAsset = $getNelmioAsset;
}
public function getFormat(): string
{
return RenderOpenApi::HTML;
}
public function render(OpenApi $spec, array $options = []): string
{
$options += [
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
];
$this->twig->addFunction($this->getNelmioAsset->toTwigFunction($options['assets_mode']));
return $this->twig->render(
'@NelmioApiDoc/SwaggerUi/index.html.twig',
[
'swagger_data' => ['spec' => json_decode($spec->toJson(), true)],
'assets_mode' => $options['assets_mode'],
'swagger_ui_config' => $options['swagger_ui_config'],
]
);
}
}

View File

@ -0,0 +1,37 @@
<?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;
/**
* @internal
*/
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,24 @@
<?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;
/**
* @internal
*/
interface OpenApiRenderer
{
public function getFormat(): string;
public function render(OpenApi $spec, array $options = []): string;
}

78
Render/RenderOpenApi.php Normal file
View File

@ -0,0 +1,78 @@
<?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 OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
class RenderOpenApi
{
public const HTML = 'html';
public const JSON = 'json';
public const YAML = 'yaml';
/** @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;
}
}
public function getAvailableFormats(): array
{
return array_keys($this->openApiRenderers);
}
public function renderFromRequest(Request $request, string $format, $area, array $extraOptions = [])
{
$options = [];
if ('' !== $request->getBaseUrl()) {
$options += [
'server_url' => $request->getSchemeAndHttpHost().$request->getBaseUrl(),
];
}
$options += $extraOptions;
return $this->render($format, $area, $options);
}
/**
* @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();
if (array_key_exists('server_url', $options) && $options['server_url']) {
$spec->servers = [new Server(['url' => $options['server_url']])];
}
return $this->openApiRenderers[$format]->render($spec, $options);
}
}

View File

@ -0,0 +1,32 @@
<?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\Yaml;
use Nelmio\ApiDocBundle\Render\OpenApiRenderer;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use OpenApi\Annotations\OpenApi;
/**
* @internal
*/
class YamlOpenApiRenderer implements OpenApiRenderer
{
public function getFormat(): string
{
return RenderOpenApi::YAML;
}
public function render(OpenApi $spec, array $options = []): string
{
return $spec->toYaml();
}
}

View File

@ -6,24 +6,42 @@
<services>
<!-- Commands -->
<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" />
</service>
<!-- Controllers -->
<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="twig" />
<argument type="service" id="nelmio_api_doc.render_docs" />
</service>
<service id="nelmio_api_doc.controller.swagger" alias="nelmio_api_doc.controller.swagger_json" public="true" />
<service id="nelmio_api_doc.controller.swagger_json" class="Nelmio\ApiDocBundle\Controller\DocumentationController" public="true">
<argument type="service" id="nelmio_api_doc.generator_locator" />
<argument type="service" id="nelmio_api_doc.render_docs" />
</service>
<service id="nelmio_api_doc.controller.swagger_yaml" class="Nelmio\ApiDocBundle\Controller\YamlDocumentationController" public="true">
<argument type="service" id="nelmio_api_doc.render_docs" />
</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" />
<argument type="service" id="nelmio_api_doc.render_docs.yaml" />
</service>
<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="nelmio_api_doc.render_docs.html.asset" />
</service>
<service id="nelmio_api_doc.render_docs.html.asset" class="Nelmio\ApiDocBundle\Render\Html\GetNelmioAsset" public="false">
<argument type="service" id="twig.extension.assets" />
</service>
<service id="nelmio_api_doc.render_docs.json" class="Nelmio\ApiDocBundle\Render\Json\JsonOpenApiRenderer" public="false">
</service>
<service id="nelmio_api_doc.render_docs.yaml" class="Nelmio\ApiDocBundle\Render\Yaml\YamlOpenApiRenderer" public="false">
</service>
<!-- Swagger Spec Generator -->

View File

@ -0,0 +1,45 @@
Commands
========
A command is provided in order to dump the documentation in ``json``, ``yaml`` 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
Every format can override API url. Useful if static documentation is not hosted on API url:
.. code-block:: bash
$ php app/console api:doc:dump --format=yaml --server-url "http://example.com/api" > api.yaml
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
alternative_names
customization
commands
faq
.. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html

View File

@ -14,8 +14,8 @@ file that was distributed with this source code. #}
<title>{% block title %}{{ swagger_data.spec.info.title }}{% endblock title %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui.css') }}">
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/style.css') }}">
{{ nelmioAsset('swagger-ui/swagger-ui.css') }}
{{ nelmioAsset('style.css') }}
{% endblock stylesheets %}
{% block swagger_data %}
@ -53,7 +53,9 @@ file that was distributed with this source code. #}
{% endblock svg_icons %}
<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">
<img src="{{ nelmioAsset('logo.png') }}" alt="NelmioApiDocBundle">
</a>
{% endblock header %}
</header>
@ -62,14 +64,18 @@ file that was distributed with this source code. #}
{% endblock %}
{% block javascripts %}
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-bundle.js') }}"></script>
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-standalone-preset.js') }}"></script>
{{ nelmioAsset('swagger-ui/swagger-ui-bundle.js') }}
{{ nelmioAsset('swagger-ui/swagger-ui-standalone-preset.js') }}
{% endblock javascripts %}
<script src="{{ asset('bundles/nelmioapidoc/init-swagger-ui.js') }}"></script>
{{ nelmioAsset('init-swagger-ui.js') }}
{% block swagger_initialization %}
<script type="text/javascript">
window.onload = loadSwaggerUI();
(function () {
var swaggerUI = {{ swagger_ui_config|json_encode(65)|raw }};
window.onload = loadSwaggerUI(swaggerUI);
})();
</script>
{% endblock swagger_initialization %}
</body>

View File

@ -11,26 +11,104 @@
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;
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],
];
}
public function testYaml()
{
$output = $this->executeDumpCommand([
'--format' => 'yaml',
'--server-url' => 'http://example.com/api',
]);
$expectedYaml = <<<YAML
servers:
-
url: 'http://example.com/api'
YAML;
self::assertStringContainsString($expectedYaml, $output);
}
/** @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)
{
$kernel = static::bootKernel();
$application = new Application($kernel);
$command = $application->find('nelmio:apidoc:dump');
$commandTester = new CommandTester($command);
$commandTester->execute([
'--area' => 'test',
'--no-pretty' => '',
]);
$commandTester->execute($options);
// the output of the command in the console
$output = $commandTester->getDisplay();
$this->assertEquals(json_encode($this->getOpenApiDefinition('test'))."\n", $output);
return $commandTester->getDisplay();
}
}

View File

@ -0,0 +1,112 @@
<?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\Html;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\Html\GetNelmioAsset;
use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase;
class GetNelmioAssetTest extends WebTestCase
{
/** @dataProvider provideAsset */
public function test($mode, $asset, $expectedContent)
{
static::bootKernel();
/** @var GetNelmioAsset $getNelmioAsset */
$getNelmioAsset = static::$container->get('nelmio_api_doc.render_docs.html.asset');
$twigFunction = $getNelmioAsset->toTwigFunction($mode);
self::assertSame($expectedContent, $twigFunction->getCallable()->__invoke($asset, $mode));
}
public function provideAsset()
{
$cdnDir = 'https://cdn.jsdelivr.net/gh/nelmio/NelmioApiDocBundle/Resources/public';
$resourceDir = __DIR__.'/../../../Resources/public';
return $this->provideCss($cdnDir, $resourceDir)
+ $this->provideJs($cdnDir, $resourceDir)
+ $this->provideImage($cdnDir);
}
private function provideCss($cdnDir, $resourceDir)
{
return [
'bundled css' => [
AssetsMode::BUNDLE,
'style.css',
'<link rel="stylesheet" href="/bundles/nelmioapidoc/style.css">',
],
'cdn css' => [
AssetsMode::CDN,
'style.css',
'<link rel="stylesheet" href="'.$cdnDir.'/style.css">',
],
'offline css' => [
AssetsMode::OFFLINE,
'style.css',
'<style>'.file_get_contents($resourceDir.'/style.css').'</style>',
],
'external css' => [
AssetsMode::BUNDLE,
'https://cdn.com/my.css',
'<link rel="stylesheet" href="https://cdn.com/my.css">',
],
];
}
private function provideJs($cdnDir, $resourceDir)
{
return [
'bundled js' => [
AssetsMode::BUNDLE,
'init-swagger-ui.js',
'<script src="/bundles/nelmioapidoc/init-swagger-ui.js"></script>',
],
'cdn js' => [
AssetsMode::CDN,
'init-swagger-ui.js',
'<script src="'.$cdnDir.'/init-swagger-ui.js"></script>',
],
'offline js' => [
AssetsMode::OFFLINE,
'init-swagger-ui.js',
'<script>'.file_get_contents($resourceDir.'/init-swagger-ui.js').'</script>',
],
'external js' => [
AssetsMode::BUNDLE,
'https://cdn.com/my.js',
'<script src="https://cdn.com/my.js"></script>',
],
];
}
private function provideImage($cdnDir)
{
return [
'bundled image' => [
AssetsMode::BUNDLE,
'logo.png',
'/bundles/nelmioapidoc/logo.png',
],
'cdn image' => [
AssetsMode::CDN,
'logo.png',
$cdnDir.'/logo.png',
],
'offline image fallbacks to cdn' => [
AssetsMode::OFFLINE,
'logo.png',
$cdnDir.'/logo.png',
],
];
}
}

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, []);
}
}