Skip to content

Commit 8435f25

Browse files
GregoireHebertdunglas
authored andcommitted
Add Redoc UI (#2384)
1 parent 538fba0 commit 8435f25

File tree

14 files changed

+227
-52
lines changed

14 files changed

+227
-52
lines changed

src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2727

2828
/**
29-
* Displays the documentation in Swagger UI.
29+
* Displays the documentation.
3030
*
3131
* @author Kévin Dunglas <[email protected]>
3232
*/
@@ -51,11 +51,14 @@ final class SwaggerUiAction
5151
private $oauthAuthorizationUrl;
5252
private $oauthScopes;
5353
private $formatsProvider;
54+
private $swaggerUiEnabled;
55+
private $reDocEnabled;
56+
private $graphqlEnabled;
5457

5558
/**
5659
* @throws InvalidArgumentException
5760
*/
58-
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, \Twig_Environment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', /* FormatsProviderInterface */ $formatsProvider = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true)
61+
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, \Twig_Environment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', /* FormatsProviderInterface */ $formatsProvider = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false)
5962
{
6063
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
6164
$this->resourceMetadataFactory = $resourceMetadataFactory;
@@ -74,6 +77,9 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
7477
$this->oauthTokenUrl = $oauthTokenUrl;
7578
$this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
7679
$this->oauthScopes = $oauthScopes;
80+
$this->swaggerUiEnabled = $swaggerUiEnabled;
81+
$this->reDocEnabled = $reDocEnabled;
82+
$this->graphqlEnabled = $graphqlEnabled;
7783

7884
if (\is_array($formatsProvider)) {
7985
if ($formatsProvider) {
@@ -113,11 +119,19 @@ private function getContext(Request $request, Documentation $documentation): arr
113119
'description' => $this->description,
114120
'formats' => $this->formats,
115121
'showWebby' => $this->showWebby,
122+
'swaggerUiEnabled' => $this->swaggerUiEnabled,
123+
'reDocEnabled' => $this->reDocEnabled,
124+
'graphqlEnabled' => $this->graphqlEnabled,
116125
];
117126

127+
$swaggerContext = ['spec_version' => $request->query->getInt('spec_version', 3)];
128+
if ('' !== $baseUrl = $request->getBaseUrl()) {
129+
$swaggerContext['base_url'] = $baseUrl;
130+
}
131+
118132
$swaggerData = [
119133
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
120-
'spec' => $this->normalizer->normalize($documentation, 'json', ['base_url' => $request->getBaseUrl(), 'spec_version' => $request->query->getInt('spec_version', 3)]),
134+
'spec' => $this->normalizer->normalize($documentation, 'json', $swaggerContext),
121135
];
122136

123137
$swaggerData['oauth'] = [

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ private function registerApiKeysConfiguration(ContainerBuilder $container, array
322322
}
323323

324324
/**
325-
* Registers the Swagger and Swagger UI configuration.
325+
* Registers the Swagger, ReDoc and Swagger UI configuration.
326326
*/
327327
private function registerSwaggerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader)
328328
{
@@ -332,9 +332,10 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array
332332

333333
$loader->load('swagger.xml');
334334

335-
if ($config['enable_swagger_ui']) {
335+
if ($config['enable_swagger_ui'] || $config['enable_re_doc']) {
336336
$loader->load('swagger-ui.xml');
337337
$container->setParameter('api_platform.enable_swagger_ui', $config['enable_swagger_ui']);
338+
$container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']);
338339
}
339340

340341
$container->setParameter('api_platform.enable_swagger', $config['enable_swagger']);

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ public function getConfigTreeBuilder()
9797
->info('Enable the NelmioApiDocBundle integration.')
9898
->end()
9999
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end()
100-
->booleanNode('enable_swagger_ui')->defaultValue(class_exists(TwigBundle::class))->info('Enable Swagger ui.')->end()
100+
->booleanNode('enable_swagger_ui')->defaultValue(class_exists(TwigBundle::class))->info('Enable Swagger UI')->end()
101+
->booleanNode('enable_re_doc')->defaultValue(class_exists(TwigBundle::class))->info('Enable ReDoc')->end()
101102
->booleanNode('enable_entrypoint')->defaultTrue()->info('Enable the entrypoint')->end()
102103
->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end()
103104
->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end()

src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@
1010
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
1111
</service>
1212

13+
<service id="api_platform.swagger.action.ui" class="ApiPlatform\Core\Bridge\Symfony\Bundle\Action\SwaggerUiAction" public="true">
14+
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
15+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
16+
<argument type="service" id="api_platform.serializer" />
17+
<argument type="service" id="twig" />
18+
<argument type="service" id="router" />
19+
<argument>%api_platform.title%</argument>
20+
<argument>%api_platform.description%</argument>
21+
<argument>%api_platform.version%</argument>
22+
<argument type="service" id="api_platform.formats_provider" />
23+
<argument>%api_platform.oauth.enabled%</argument>
24+
<argument>%api_platform.oauth.clientId%</argument>
25+
<argument>%api_platform.oauth.clientSecret%</argument>
26+
<argument>%api_platform.oauth.type%</argument>
27+
<argument>%api_platform.oauth.flow%</argument>
28+
<argument>%api_platform.oauth.tokenUrl%</argument>
29+
<argument>%api_platform.oauth.authorizationUrl%</argument>
30+
<argument>%api_platform.oauth.scopes%</argument>
31+
<argument>%api_platform.show_webby%</argument>
32+
<argument>%api_platform.enable_swagger_ui%</argument>
33+
<argument>%api_platform.enable_re_doc%</argument>
34+
<argument>%api_platform.graphql.enabled%</argument>
35+
</service>
36+
1337
</services>
1438

1539
</container>

src/Bridge/Symfony/Bundle/Resources/config/swagger.xml

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,6 @@
4949
<tag name="console.command" />
5050
</service>
5151

52-
<!-- Action -->
53-
54-
<service id="api_platform.swagger.action.ui" class="ApiPlatform\Core\Bridge\Symfony\Bundle\Action\SwaggerUiAction" public="true">
55-
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
56-
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
57-
<argument type="service" id="api_platform.serializer" />
58-
<argument type="service" id="twig" />
59-
<argument type="service" id="router" />
60-
<argument>%api_platform.title%</argument>
61-
<argument>%api_platform.description%</argument>
62-
<argument>%api_platform.version%</argument>
63-
<argument type="service" id="api_platform.formats_provider" />
64-
<argument>%api_platform.oauth.enabled%</argument>
65-
<argument>%api_platform.oauth.clientId%</argument>
66-
<argument>%api_platform.oauth.clientSecret%</argument>
67-
<argument>%api_platform.oauth.type%</argument>
68-
<argument>%api_platform.oauth.flow%</argument>
69-
<argument>%api_platform.oauth.tokenUrl%</argument>
70-
<argument>%api_platform.oauth.authorizationUrl%</argument>
71-
<argument>%api_platform.oauth.scopes%</argument>
72-
<argument>%api_platform.show_webby%</argument>
73-
</service>
74-
7552
</services>
7653

7754
</container>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
window.onload = () => {
4+
const data = JSON.parse(document.getElementById('swagger-data').innerText);
5+
6+
Redoc.init(data.spec, {}, document.getElementById('swagger-ui'));
7+
};

src/Bridge/Symfony/Bundle/Resources/public/redoc/redoc.standalone.js

Lines changed: 114 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Bridge/Symfony/Bundle/Resources/views/SwaggerUi/index.html.twig

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,24 @@
6464
{% for format in formats|keys %}
6565
<a href="{{ path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')|merge({'_format': format})) }}">{{ format }}</a>
6666
{% endfor %}
67+
<br>
68+
Other API docs:
69+
{% set active_ui = app.request.get('ui', 'swagger_ui') %}
70+
{% if swaggerUiEnabled and active_ui != 'swagger_ui' %}<a href="{{ path('api_entrypoint') }}">Swagger UI</a>{% endif %}
71+
{% if reDocEnabled and active_ui != 're_doc' %}<a href="{{ path('api_entrypoint', {'ui': 're_doc'}) }}">ReDoc</a>{% endif %}
72+
<a href="{% if graphqlEnabled %}{{ path('api_graphql_entrypoint') }}{% else %}javascript:alert('GraphQL support is not enabled, see https://api-platform.com/docs/core/graphql/'){% endif %}">GraphiQL</a>
6773
</div>
6874
</div>
6975
</div>
7076

71-
<script src="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui-bundle.js') }}"></script>
72-
<script src="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui-standalone-preset.js') }}"></script>
73-
<script src="{{ asset('bundles/apiplatform/init-swagger-ui.js') }}"></script>
77+
{% if ((false == swaggerUiEnabled and reDocEnabled) or (reDocEnabled and active_ui == 're_doc')) %}
78+
<script src="{{ asset('bundles/apiplatform/redoc/redoc.standalone.js') }}"></script>
79+
<script src="{{ asset('bundles/apiplatform/init-redoc-ui.js') }}"></script>
80+
{% elseif (swaggerUiEnabled) %}
81+
<script src="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui-bundle.js') }}"></script>
82+
<script src="{{ asset('bundles/apiplatform/swagger-ui/swagger-ui-standalone-preset.js') }}"></script>
83+
<script src="{{ asset('bundles/apiplatform/init-swagger-ui.js') }}"></script>
84+
{% endif %}
7485

7586
</body>
7687
</html>

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -659,15 +659,17 @@ private function getType(bool $v3, string $type, bool $isCollection, string $cla
659659

660660
private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
661661
{
662+
$baseUrl = $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];
663+
662664
if ($v3) {
663-
$docs = [
664-
'openapi' => self::OPENAPI_VERSION,
665-
'servers' => [['url' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL]]],
666-
];
665+
$docs = ['openapi' => self::OPENAPI_VERSION];
666+
if ('/' !== $baseUrl) {
667+
$docs['servers'] = [['url' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL]]];
668+
}
667669
} else {
668670
$docs = [
669671
'swagger' => self::SWAGGER_VERSION,
670-
'basePath' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL],
672+
'basePath' => $baseUrl,
671673
];
672674
}
673675

tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ public function testInvoke(Request $request, ProphecyInterface $twigProphecy)
6767

6868
public function getInvokeParameters()
6969
{
70-
$postRequest = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'post']);
71-
$postRequest->setMethod('POST');
72-
7370
$twigCollectionProphecy = $this->prophesize(\Twig_Environment::class);
7471
$twigCollectionProphecy->render('@ApiPlatform/SwaggerUi/index.html.twig', [
7572
'title' => '',
7673
'description' => '',
7774
'formats' => [],
7875
'showWebby' => true,
76+
'swaggerUiEnabled' => false,
77+
'reDocEnabled' => false,
78+
'graphqlEnabled' => false,
7979
'swagger_data' => [
8080
'url' => '/url',
8181
'spec' => self::SPEC,
@@ -103,7 +103,10 @@ public function getInvokeParameters()
103103
'title' => '',
104104
'description' => '',
105105
'formats' => [],
106+
'swaggerUiEnabled' => false,
106107
'showWebby' => true,
108+
'reDocEnabled' => false,
109+
'graphqlEnabled' => false,
107110
'swagger_data' => [
108111
'url' => '/url',
109112
'spec' => self::SPEC,
@@ -129,6 +132,7 @@ public function getInvokeParameters()
129132
return [
130133
[new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']), $twigCollectionProphecy],
131134
[new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get']), $twigItemProphecy],
135+
[new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get'], [], [], ['REQUEST_URI' => '/docs', 'SCRIPT_FILENAME' => '/docs']), $twigItemProphecy],
132136
];
133137
}
134138

@@ -151,6 +155,9 @@ public function testDoNotRunCurrentRequest(Request $request)
151155
'description' => '',
152156
'formats' => [],
153157
'showWebby' => true,
158+
'swaggerUiEnabled' => false,
159+
'reDocEnabled' => false,
160+
'graphqlEnabled' => false,
154161
'swagger_data' => [
155162
'url' => '/url',
156163
'spec' => self::SPEC,

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,25 @@ public function testDisabledDocsRemovesAddLinkHeaderService()
415415
$this->extension->load($config, $containerBuilder);
416416
}
417417

418+
public function testDisabledSwaggerUIAndRedoc()
419+
{
420+
$containerBuilderProphecy = $this->getBaseContainerBuilderProphecy();
421+
$containerBuilderProphecy->setDefinition('api_platform.swagger.action.ui', Argument::type(Definition::class))->shouldNotBeCalled();
422+
$containerBuilderProphecy->setDefinition('api_platform.swagger.listener.ui', Argument::type(Definition::class))->shouldNotBeCalled();
423+
$containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', true)->shouldNotBeCalled();
424+
$containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', true)->shouldNotBeCalled();
425+
$containerBuilderProphecy->setParameter('api_platform.enable_swagger_ui', false)->shouldNotBeCalled();
426+
$containerBuilderProphecy->setParameter('api_platform.enable_re_doc', true)->shouldNotBeCalled();
427+
$containerBuilderProphecy->setParameter('api_platform.enable_re_doc', false)->shouldNotBeCalled();
428+
$containerBuilder = $containerBuilderProphecy->reveal();
429+
430+
$config = self::DEFAULT_CONFIG;
431+
$config['api_platform']['enable_swagger_ui'] = false;
432+
$config['api_platform']['enable_re_doc'] = false;
433+
434+
$this->extension->load($config, $containerBuilder);
435+
}
436+
418437
private function getPartialContainerBuilderProphecy($test = false)
419438
{
420439
$containerBuilderProphecy = $this->prophesize(ContainerBuilder::class);
@@ -667,6 +686,7 @@ private function getBaseContainerBuilderProphecy()
667686
'api_platform.swagger.api_keys' => [],
668687
'api_platform.enable_swagger' => true,
669688
'api_platform.enable_swagger_ui' => true,
689+
'api_platform.enable_re_doc' => true,
670690
'api_platform.graphql.enabled' => true,
671691
'api_platform.graphql.graphiql.enabled' => true,
672692
'api_platform.resource_class_directories' => Argument::type('array'),
@@ -758,7 +778,6 @@ private function getBaseContainerBuilderProphecy()
758778
'api_platform.problem.encoder',
759779
'api_platform.problem.normalizer.constraint_violation_list',
760780
'api_platform.problem.normalizer.error',
761-
'api_platform.swagger.action.ui',
762781
'api_platform.swagger.command.swagger_command',
763782
'api_platform.http_cache.listener.response.configure',
764783
'api_platform.http_cache.purger.varnish',

tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public function testDefaultConfig()
8686
'enable_swagger' => true,
8787
'enable_swagger_ui' => true,
8888
'enable_entrypoint' => true,
89+
'enable_re_doc' => true,
8990
'enable_docs' => true,
9091
'enable_profiler' => true,
9192
'graphql' => [

tests/Swagger/Serializer/DocumentationNormalizerV3Test.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,6 @@ public function testNormalizeWithNameConverter()
457457
],
458458
],
459459
'security' => [['oauth' => []]],
460-
'servers' => [['url' => '/']],
461460
];
462461

463462
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -823,7 +822,6 @@ public function testNormalizeWithOnlyNormalizationGroups()
823822
]),
824823
]),
825824
],
826-
'servers' => [['url' => '/']],
827825
];
828826

829827
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -1167,7 +1165,6 @@ public function testNormalizeWithOnlyDenormalizationGroups()
11671165
]),
11681166
]),
11691167
],
1170-
'servers' => [['url' => '/']],
11711168
];
11721169

11731170
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -1396,7 +1393,6 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups()
13961393
]),
13971394
]),
13981395
],
1399-
'servers' => [['url' => '/']],
14001396
];
14011397

14021398
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -1611,7 +1607,6 @@ public function testNoOperations()
16111607
'version' => '0.0.0',
16121608
],
16131609
'paths' => new \ArrayObject([]),
1614-
'servers' => [['url' => '/']],
16151610
];
16161611

16171612
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -1686,7 +1681,6 @@ public function testWithCustomMethod()
16861681
]),
16871682
],
16881683
]),
1689-
'servers' => [['url' => '/']],
16901684
];
16911685

16921686
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -1946,7 +1940,6 @@ public function testNormalizeWithNestedNormalizationGroups()
19461940
]),
19471941
]),
19481942
],
1949-
'servers' => [['url' => '/']],
19501943
];
19511944

19521945
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -2086,7 +2079,6 @@ private function normalizeWithFilters($filterLocator)
20862079
]),
20872080
]),
20882081
],
2089-
'servers' => [['url' => '/']],
20902082
];
20912083

20922084
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -2257,7 +2249,6 @@ public function testNormalizeWithSubResource()
22572249
]),
22582250
]),
22592251
],
2260-
'servers' => [['url' => '/']],
22612252
];
22622253

22632254
$this->assertEquals($expected, $normalizer->normalize($documentation));
@@ -2560,7 +2551,6 @@ public function testNormalizeWithCustomFormatsDefinedAtOperationLevel()
25602551

25612552
$expected = [
25622553
'openapi' => '3.0.2',
2563-
'servers' => [['url' => '/']],
25642554
'info' => [
25652555
'title' => 'Test API',
25662556
'description' => 'This is a test API.',

0 commit comments

Comments
 (0)