diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de0cf7a9..d46035549 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/config/services.xml b/config/services.xml index 469e87de9..75873f9c7 100644 --- a/config/services.xml +++ b/config/services.xml @@ -40,6 +40,7 @@ + diff --git a/docs/commands.rst b/docs/commands.rst index fb54519bf..acd294f60 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -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/ diff --git a/docs/customization.rst b/docs/customization.rst index 1ccbdc5fa..27700481c 100644 --- a/docs/customization.rst +++ b/docs/customization.rst @@ -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 %} + {% endblock %} + + {# + Change Redocly configuration + All parameters are explained on Redocly website: + https://redocly.com/docs/redoc/config/ + #} + {% block swagger_initialization %} + {% endblock %} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dae21d11e..6c228bec1 100644 --- a/phpstan-baseline.neon +++ b/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 diff --git a/public/init-redocly-ui.js b/public/init-redocly-ui.js index 0ea230451..39dcada2b 100644 --- a/public/init-redocly-ui.js +++ b/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')); -}; \ No newline at end of file + Redoc.init(data.spec, userOptions, document.getElementById('swagger-ui')); +} diff --git a/src/Command/DumpCommand.php b/src/Command/DumpCommand.php index 900626025..b9eada346 100644 --- a/src/Command/DumpCommand.php +++ b/src/Command/DumpCommand.php @@ -18,6 +18,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @final + */ class DumpCommand extends Command { private RenderOpenApi $renderOpenApi; @@ -28,6 +31,7 @@ class DumpCommand extends Command private $defaultHtmlConfig = [ 'assets_mode' => AssetsMode::CDN, 'swagger_ui_config' => [], + 'redocly_config' => [], ]; public function __construct(RenderOpenApi $renderOpenApi) @@ -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']), diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 73b5cf56a..bdf45576e 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -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; @@ -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( diff --git a/src/DependencyInjection/NelmioApiDocExtension.php b/src/DependencyInjection/NelmioApiDocExtension.php index 0a70868a3..81f927330 100644 --- a/src/DependencyInjection/NelmioApiDocExtension.php +++ b/src/DependencyInjection/NelmioApiDocExtension.php @@ -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 diff --git a/src/Render/Html/HtmlOpenApiRenderer.php b/src/Render/Html/HtmlOpenApiRenderer.php index edb47d6c9..4794e4abe 100644 --- a/src/Render/Html/HtmlOpenApiRenderer.php +++ b/src/Render/Html/HtmlOpenApiRenderer.php @@ -23,16 +23,20 @@ class HtmlOpenApiRenderer implements OpenApiRenderer { /** @var Environment|\Twig_Environment */ private $twig; + /** @var array */ + private array $htmlConfig; /** * @param Environment|\Twig_Environment $twig + * @param array $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 @@ -42,10 +46,7 @@ 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( @@ -53,6 +54,7 @@ public function render(OpenApi $spec, array $options = []): string [ 'swagger_data' => ['spec' => json_decode($spec->toJson(), true)], 'assets_mode' => $options['assets_mode'], + 'redocly_config' => $options['redocly_config'], ] ); } diff --git a/templates/Redocly/index.html.twig b/templates/Redocly/index.html.twig index 1890652b7..8ca5483dd 100644 --- a/templates/Redocly/index.html.twig +++ b/templates/Redocly/index.html.twig @@ -35,4 +35,13 @@ {% endblock javascripts %} {{ nelmioAsset(assets_mode, 'init-redocly-ui.js') }} - \ No newline at end of file + + {% block swagger_initialization %} + + {% endblock swagger_initialization %} + + diff --git a/templates/SwaggerUi/index.html.twig b/templates/SwaggerUi/index.html.twig index d1f0097ab..efe852405 100644 --- a/templates/SwaggerUi/index.html.twig +++ b/templates/SwaggerUi/index.html.twig @@ -75,10 +75,9 @@ file that was distributed with this source code. #} {% block swagger_initialization %} {% endblock swagger_initialization %} diff --git a/tests/Command/DumpCommandTest.php b/tests/Command/DumpCommandTest.php index 5c4374513..787ab5b9c 100644 --- a/tests/Command/DumpCommandTest.php +++ b/tests/Command/DumpCommandTest.php @@ -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; @@ -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', diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 29ca68211..6e4d87a02 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -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( [ @@ -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' => [], @@ -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' => [ [ @@ -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.', + ]; + } } diff --git a/tests/DependencyInjection/NelmioApiDocExtensionTest.php b/tests/DependencyInjection/NelmioApiDocExtensionTest.php index c62bde577..1e37bcf9d 100644 --- a/tests/DependencyInjection/NelmioApiDocExtensionTest.php +++ b/tests/DependencyInjection/NelmioApiDocExtensionTest.php @@ -13,6 +13,7 @@ use Nelmio\ApiDocBundle\DependencyInjection\NelmioApiDocExtension; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -299,4 +300,68 @@ public static function provideCacheConfig(): \Generator ], ]; } + + /** + * @dataProvider provideOpenApiRendererWithHtmlConfig + * + * @param array $htmlConfig + * @param array $expectedHtmlConfig + */ + public function testHtmlOpenApiRendererWithHtmlConfig(array $htmlConfig, array $expectedHtmlConfig): void + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.bundles', [ + 'TwigBundle' => TwigBundle::class, + ]); + + $extension = new NelmioApiDocExtension(); + $extension->load([['html_config' => $htmlConfig]], $container); + + $argument = $container->getDefinition('nelmio_api_doc.render_docs.html')->getArgument(1); + self::assertSame($expectedHtmlConfig, $argument); + } + + public static function provideOpenApiRendererWithHtmlConfig(): \Generator + { + yield 'default' => [ + [], + [ + 'assets_mode' => 'cdn', + 'swagger_ui_config' => [], + 'redocly_config' => [], + ], + ]; + yield 'swagger_ui' => [ + [ + 'assets_mode' => 'bundle', + 'swagger_ui_config' => [ + 'deepLinking' => true, + ], + ], + [ + 'assets_mode' => 'bundle', + 'swagger_ui_config' => [ + 'deepLinking' => true, + ], + 'redocly_config' => [], + ], + ]; + yield 'redocly' => [ + [ + 'assets_mode' => 'cdn', + 'redocly_config' => [ + 'expandResponses' => '200,201', + 'hideDownloadButton' => true, + ], + ], + [ + 'assets_mode' => 'cdn', + 'redocly_config' => [ + 'expandResponses' => '200,201', + 'hideDownloadButton' => true, + ], + 'swagger_ui_config' => [], + ], + ]; + } }