Skip to content

Commit

Permalink
feat: Add ability to configure UI through configuration (#2251)
Browse files Browse the repository at this point in the history
* Add ability to configure UI through configuration

* Review fixes

* Review fixes 2

* Fix PHPStan

* use generator return typehint

* update changelog

* mark DumpCommand as final

* update phpstan baseline

* add swagger & redocly info configuration url

* add html_config.assets_mode configuration validation

* add more invalid configuration test cases

* fix redoc config url

---------

Co-authored-by: DjordyKoert <djordy.koert@yoursurprise.com>
  • Loading branch information
HypeMC and DjordyKoert committed Apr 19, 2024
1 parent 38682dd commit 5669b8f
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 35 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
@@ -1,6 +1,20 @@
CHANGELOG
=========

4.26.0
-----
* Add ability to configure UI through configuration
```yaml
nelmio_api_doc:
html_config:
assets_mode: bundle
redocly_config:
expandResponses: '200,201'
hideDownloadButton: true
swagger_ui_config:
deepLinking: true
```

4.25.0
-----
* Added support for [JMS @Discriminator](https://jmsyst.com/libs/serializer/master/reference/annotations#discriminator) annotation/attribute
Expand Down
1 change: 1 addition & 0 deletions config/services.xml
Expand Up @@ -40,6 +40,7 @@
</service>
<service id="nelmio_api_doc.render_docs.html" class="Nelmio\ApiDocBundle\Render\Html\HtmlOpenApiRenderer" public="false">
<argument type="service" id="twig" />
<argument type="collection" />
</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" />
Expand Down
2 changes: 2 additions & 0 deletions docs/commands.rst
Expand Up @@ -37,9 +37,11 @@ or configure UI configuration, use the ``--html-config`` option.
- ``server_url`` - API url, useful if static documentation is not hosted on API url
- ``swagger_ui_config`` - `configure Swagger UI`_
- ``"supportedSubmitMethods":[]`` disables the sandbox
- ``redocly_config`` - `configure Redocly`_

.. code-block:: bash
$ php bin/console nelmio:apidoc: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/
.. _`configure Redocly`: https://redocly.com/docs/redoc/config/
28 changes: 23 additions & 5 deletions docs/customization.rst
Expand Up @@ -25,16 +25,34 @@ Just create a file ``templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.t
{% extends '@!NelmioApiDoc/SwaggerUi/index.html.twig' %}
{#
Change swagger UI configuration
Change Swagger UI configuration
All parameters are explained on Swagger UI website:
https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
#}
{% block swagger_initialization %}
<script type="text/javascript">
window.onload = loadSwaggerUI({
defaultModelsExpandDepth: -1,
deepLinking: true,
});
window.onload = () => {
loadSwaggerUI({
defaultModelsExpandDepth: -1,
deepLinking: true,
});
};
</script>
{% endblock %}
{#
Change Redocly configuration
All parameters are explained on Redocly website:
https://redocly.com/docs/redoc/config/
#}
{% block swagger_initialization %}
<script type="text/javascript">
window.onload = () => {
loadRedocly({
expandResponses: '200,201',
hideDownloadButton: true,
});
};
</script>
{% endblock %}
Expand Down
10 changes: 0 additions & 10 deletions phpstan-baseline.neon
@@ -1,15 +1,5 @@
parameters:
ignoreErrors:
-
message: "#^Property Nelmio\\\\ApiDocBundle\\\\Annotation\\\\Model\\:\\:\\$_required type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Annotation/Model.php

-
message: "#^Property Nelmio\\\\ApiDocBundle\\\\Annotation\\\\Security\\:\\:\\$_required type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Annotation/Security.php

-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
Expand Down
6 changes: 3 additions & 3 deletions public/init-redocly-ui.js
@@ -1,7 +1,7 @@
'use strict';

window.onload = () => {
function loadRedocly(userOptions = {}) {
const data = JSON.parse(document.getElementById('swagger-data').innerText);

Redoc.init(data.spec, {}, document.getElementById('swagger-ui'));
};
Redoc.init(data.spec, userOptions, document.getElementById('swagger-ui'));
}
6 changes: 5 additions & 1 deletion src/Command/DumpCommand.php
Expand Up @@ -18,6 +18,9 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* @final
*/
class DumpCommand extends Command
{
private RenderOpenApi $renderOpenApi;
Expand All @@ -28,6 +31,7 @@ class DumpCommand extends Command
private $defaultHtmlConfig = [
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
'redocly_config' => [],
];

public function __construct(RenderOpenApi $renderOpenApi)
Expand Down Expand Up @@ -67,7 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$options = [];
if (RenderOpenApi::HTML === $format) {
$rawHtmlConfig = json_decode($input->getOption('html-config'), true);
$options = is_array($rawHtmlConfig) ? $rawHtmlConfig : $this->defaultHtmlConfig;
$options = is_array($rawHtmlConfig) ? $rawHtmlConfig + $this->defaultHtmlConfig : $this->defaultHtmlConfig;
} elseif (RenderOpenApi::JSON === $format) {
$options = [
'no-pretty' => $input->hasParameterOption(['--no-pretty']),
Expand Down
24 changes: 24 additions & 0 deletions src/DependencyInjection/Configuration.php
Expand Up @@ -11,6 +11,7 @@

namespace Nelmio\ApiDocBundle\DependencyInjection;

use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

Expand Down Expand Up @@ -55,6 +56,29 @@ public function getConfigTreeBuilder(): TreeBuilder
->defaultValue(['json'])
->prototype('scalar')->end()
->end()
->arrayNode('html_config')
->info('UI configuration options')
->addDefaultsIfNotSet()
->children()
->scalarNode('assets_mode')
->defaultValue(AssetsMode::CDN)
->validate()
->ifNotInArray([AssetsMode::BUNDLE, AssetsMode::CDN, AssetsMode::OFFLINE])
->thenInvalid('Invalid assets mode %s')
->end()
->end()
->arrayNode('swagger_ui_config')
->info('https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/')
->addDefaultsIfNotSet()
->ignoreExtraKeys(false)
->end()
->arrayNode('redocly_config')
->info('https://redocly.com/docs/redoc/config/')
->addDefaultsIfNotSet()
->ignoreExtraKeys(false)
->end()
->end()
->end()
->arrayNode('areas')
->info('Filter the routes that are documented')
->defaultValue(
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/NelmioApiDocExtension.php
Expand Up @@ -227,6 +227,8 @@ public function load(array $configs, ContainerBuilder $container): void

$container->removeDefinition('nelmio_api_doc.render_docs.html');
$container->removeDefinition('nelmio_api_doc.render_docs.html.asset');
} elseif (isset($config['html_config'])) {
$container->getDefinition('nelmio_api_doc.render_docs.html')->replaceArgument(1, $config['html_config']);
}

// ApiPlatform support
Expand Down
12 changes: 7 additions & 5 deletions src/Render/Html/HtmlOpenApiRenderer.php
Expand Up @@ -23,16 +23,20 @@ class HtmlOpenApiRenderer implements OpenApiRenderer
{
/** @var Environment|\Twig_Environment */
private $twig;
/** @var array<string, mixed> */
private array $htmlConfig;

/**
* @param Environment|\Twig_Environment $twig
* @param array<string, mixed> $htmlConfig
*/
public function __construct($twig)
public function __construct($twig, array $htmlConfig)
{
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->htmlConfig = $htmlConfig;
}

public function getFormat(): string
Expand All @@ -42,17 +46,15 @@ public function getFormat(): string

public function render(OpenApi $spec, array $options = []): string
{
$options += [
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
];
$options += $this->htmlConfig;

if (isset($options['ui_renderer']) && Renderer::REDOCLY === $options['ui_renderer']) {
return $this->twig->render(
'@NelmioApiDoc/Redocly/index.html.twig',
[
'swagger_data' => ['spec' => json_decode($spec->toJson(), true)],
'assets_mode' => $options['assets_mode'],
'redocly_config' => $options['redocly_config'],
]
);
}
Expand Down
11 changes: 10 additions & 1 deletion templates/Redocly/index.html.twig
Expand Up @@ -35,4 +35,13 @@
{% endblock javascripts %}

{{ nelmioAsset(assets_mode, 'init-redocly-ui.js') }}
</html>

{% block swagger_initialization %}
<script type="text/javascript">
window.onload = () => {
loadRedocly({{ redocly_config|json_encode(81)|raw }});
};
</script>
{% endblock swagger_initialization %}
</body>
</html>
7 changes: 3 additions & 4 deletions templates/SwaggerUi/index.html.twig
Expand Up @@ -75,10 +75,9 @@ file that was distributed with this source code. #}

{% block swagger_initialization %}
<script type="text/javascript">
(function () {
var swaggerUI = {{ swagger_ui_config|json_encode(65)|raw }};
window.onload = loadSwaggerUI(swaggerUI);
})();
window.onload = () => {
loadSwaggerUI({{ swagger_ui_config|json_encode(65)|raw }});
};
</script>
{% endblock swagger_initialization %}
</body>
Expand Down
11 changes: 11 additions & 0 deletions tests/Command/DumpCommandTest.php
Expand Up @@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Command;

use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\Html\Renderer;
use Nelmio\ApiDocBundle\Tests\Functional\WebTestCase; // for the creation of the kernel
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
Expand Down Expand Up @@ -102,6 +103,16 @@ public static function provideAssetsMode(): \Generator
'"supportedSubmitMethods":["get"]',
];

yield 'configure redocly' => [
[
'ui_renderer' => Renderer::REDOCLY,
'redocly_config' => [
'hideDownloadButton' => true,
],
],
'"hideDownloadButton":true',
];

yield 'configure server url' => [
[
'server_url' => 'http://example.com/api',
Expand Down
87 changes: 81 additions & 6 deletions tests/DependencyInjection/ConfigurationTest.php
Expand Up @@ -13,14 +13,23 @@

use Nelmio\ApiDocBundle\DependencyInjection\Configuration;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;

class ConfigurationTest extends TestCase
{
private Processor $processor;

protected function setUp(): void
{
$this->processor = new Processor();

parent::setUp();
}

public function testDefaultArea(): void
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [['areas' => ['path_patterns' => ['/foo']]]]);
$config = $this->processor->processConfiguration(new Configuration(), [['areas' => ['path_patterns' => ['/foo']]]]);

self::assertSame(
[
Expand All @@ -39,8 +48,7 @@ public function testDefaultArea(): void

public function testAreas(): void
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [['areas' => $areas = [
$config = $this->processor->processConfiguration(new Configuration(), [['areas' => $areas = [
'default' => [
'path_patterns' => ['/foo'],
'host_patterns' => [],
Expand Down Expand Up @@ -72,8 +80,7 @@ public function testAreas(): void

public function testAlternativeNames(): void
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [[
$config = $this->processor->processConfiguration(new Configuration(), [[
'models' => [
'names' => [
[
Expand Down Expand Up @@ -148,4 +155,72 @@ public function testAlternativeNames(): void
],
], $config['models']['names']);
}

/**
* @dataProvider provideInvalidConfiguration
*
* @param mixed[] $configuration
*/
public function testInvalidConfiguration(array $configuration, string $expectedError): void
{
self::expectException(InvalidConfigurationException::class);
self::expectExceptionMessage($expectedError);

$this->processor->processConfiguration(new Configuration(), [$configuration]);
}

public static function provideInvalidConfiguration(): \Generator
{
yield 'invalid html_config.assets_mode' => [
[
'html_config' => [
'assets_mode' => 'invalid',
],
],
'Invalid assets mode "invalid"',
];

yield 'do not set cache.item_id' => [
[
'cache' => [
'pool' => null,
'item_id' => 'some-id',
],
],
'Can not set cache.item_id if cache.pool is null',
];

yield 'do not set cache.item_id, default pool' => [
[
'cache' => [
'item_id' => 'some-id',
],
],
'Can not set cache.item_id if cache.pool is null',
];

yield 'default area missing ' => [
[
'areas' => [
'some_not_default_area' => [],
],
],
'You must specify a `default` area under `nelmio_api_doc.areas`.',
];

yield 'invalid groups value for model ' => [
[
'models' => [
'names' => [
[
'alias' => 'Foo1',
'type' => 'App\Foo',
'groups' => 'invalid_string_value',
],
],
],
],
'Model groups must be either `null` or an array.',
];
}
}

0 comments on commit 5669b8f

Please sign in to comment.