Skip to content

Commit

Permalink
Merge bcf104f into 2dc666c
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jul 27, 2020
2 parents 2dc666c + bcf104f commit 36cd63c
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 3 deletions.
2 changes: 0 additions & 2 deletions features/openapi/docs.feature
Expand Up @@ -112,11 +112,9 @@ Feature: Documentation support
Then the response status code should be 200
And I should see text matching "My Dummy API"
And I should see text matching "openapi"
And I should see text matching "3.0.2"

Scenario: OpenAPI UI is enabled for an arbitrary endpoint
Given I add "Accept" header equal to "text/html"
And I send a "GET" request to "/dummies?spec_version=3"
Then the response status code should be 200
And I should see text matching "openapi"
And I should see text matching "3.0.2"
12 changes: 11 additions & 1 deletion src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Action;

use ApiPlatform\Core\Api\FormatsProviderInterface;
use ApiPlatform\Core\Bridge\Symfony\Bundle\SwaggerUi\SwaggerUiAction as OpenApiSwaggerUiAction;
use ApiPlatform\Core\Documentation\Documentation;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
Expand Down Expand Up @@ -61,7 +62,7 @@ final class SwaggerUiAction
/**
* @param int[] $swaggerVersions
*/
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, array $swaggerVersions = [2, 3])
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], bool $showWebby = true, bool $swaggerUiEnabled = false, bool $reDocEnabled = false, bool $graphqlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false, array $swaggerVersions = [2, 3], OpenApiSwaggerUiAction $swaggerUiAction = null)
{
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
Expand All @@ -86,6 +87,11 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
$this->graphiQlEnabled = $graphiQlEnabled;
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
$this->swaggerVersions = $swaggerVersions;
$this->swaggerUiAction = $swaggerUiAction;

if (null === $this->swaggerUiAction) {
@trigger_error(sprintf('The use of "%s" is deprecated since API Platform 2.6, use "%s" instead.', __CLASS__, OpenApiSwaggerUiAction::class), E_USER_DEPRECATED);
}

if (\is_array($formats)) {
$this->formats = $formats;
Expand All @@ -99,6 +105,10 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName

public function __invoke(Request $request)
{
if ($this->swaggerUiAction) {
return $this->swaggerUiAction->__invoke($request);
}

$attributes = RequestAttributesExtractor::extractAttributes($request);

// BC check to be removed in 3.0
Expand Down
24 changes: 24 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/swagger-ui.xml
Expand Up @@ -9,6 +9,7 @@
<service id="api_platform.swagger.listener.ui" class="ApiPlatform\Core\Bridge\Symfony\Bundle\EventListener\SwaggerUiListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
</service>
<service id="api_platform.swagger_ui.listener" alias="api_platform.swagger.listener.ui" />

<service id="api_platform.swagger.action.ui" class="ApiPlatform\Core\Bridge\Symfony\Bundle\Action\SwaggerUiAction" public="true">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
Expand All @@ -35,6 +36,29 @@
<argument>%api_platform.graphql.graphiql.enabled%</argument>
<argument>%api_platform.graphql.graphql_playground.enabled%</argument>
<argument>%api_platform.swagger.versions%</argument>
<argument type="service" id="api_platform.swagger_ui.action" />
</service>

<service id="api_platform.swagger_ui.context" class="ApiPlatform\Core\Bridge\Symfony\Bundle\SwaggerUi\SwaggerUiContext">
<argument>%api_platform.enable_swagger_ui%</argument>
<argument>%api_platform.show_webby%</argument>
<argument>%api_platform.enable_re_doc%</argument>
<argument>%api_platform.graphql.enabled%</argument>
<argument>%api_platform.graphql.graphiql.enabled%</argument>
<argument>%api_platform.graphql.graphql_playground.enabled%</argument>
</service>

<service id="api_platform.swagger_ui.action" class="ApiPlatform\Core\Bridge\Symfony\Bundle\SwaggerUi\SwaggerUiAction" public="true">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="twig" />
<argument type="service" id="router" />
<argument type="service" id="api_platform.serializer" />
<argument type="service" id="api_platform.openapi.factory" />
<argument type="service" id="api_platform.openapi.options" />
<argument type="service" id="api_platform.swagger_ui.context" />
<argument>%api_platform.formats%</argument>
<argument>%api_platform.oauth.clientId%</argument>
<argument>%api_platform.oauth.clientSecret%</argument>
</service>

</services>
Expand Down
Expand Up @@ -72,6 +72,7 @@
{% set active_ui = app.request.get('ui', 'swagger_ui') %}
{% if swaggerUiEnabled and active_ui != 'swagger_ui' %}<a href="{{ path('api_doc') }}">Swagger UI</a>{% endif %}
{% if reDocEnabled and active_ui != 're_doc' %}<a href="{{ path('api_doc', {'ui': 're_doc'}) }}">ReDoc</a>{% endif %}
{# FIXME: Typo in graphql => graphQl in SwaggerUiAction #}
{% if not graphqlEnabled %}<a href="javascript:alert('GraphQL support is not enabled, see https://api-platform.com/docs/core/graphql/')">GraphiQL</a>{% endif %}
{% if graphiQlEnabled %}<a href="{{ path('api_graphql_graphiql') }}">GraphiQL</a>{% endif %}
{% if graphQlPlaygroundEnabled %}<a href="{{ path('api_graphql_graphql_playground') }}">GraphQL Playground</a>{% endif %}
Expand Down
123 changes: 123 additions & 0 deletions src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiAction.php
@@ -0,0 +1,123 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\SwaggerUi;

use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\Options;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment as TwigEnvironment;

/**
* Displays the swaggerui interface.
*
* @author Antoine Bluchet <soyuka@gmail.com>
*/
final class SwaggerUiAction
{
private $twig;
private $urlGenerator;
private $normalizer;
private $openApiFactory;
private $openApiOptions;
private $swaggerUiContext;
private $formats;
private $resourceMetadataFactory;
private $oauthClientId;
private $oauthClientSecret;

public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, TwigEnvironment $twig, UrlGeneratorInterface $urlGenerator, NormalizerInterface $normalizer, OpenApiFactoryInterface $openApiFactory, Options $openApiOptions, SwaggerUiContext $swaggerUiContext, array $formats = [], string $oauthClientId = null, string $oauthClientSecret = null)
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->twig = $twig;
$this->urlGenerator = $urlGenerator;
$this->normalizer = $normalizer;
$this->openApiFactory = $openApiFactory;
$this->openApiOptions = $openApiOptions;
$this->swaggerUiContext = $swaggerUiContext;
$this->formats = $formats;
$this->oauthClientId = $oauthClientId;
$this->oauthClientSecret = $oauthClientSecret;
}

public function __invoke(Request $request)
{
$openApi = $this->openApiFactory->__invoke(['base_url' => $request->getBaseUrl() ?: '/']);

$swaggerContext = [
'formats' => $this->formats,
'title' => $openApi->getInfo()->getTitle(),
'description' => $openApi->getInfo()->getDescription(),
'showWebby' => $this->swaggerUiContext->isWebbyShown(),
'swaggerUiEnabled' => $this->swaggerUiContext->isSwaggerUiEnabled(),
'reDocEnabled' => $this->swaggerUiContext->isRedocEnabled(),
// FIXME: typo graphql => graphQl
'graphqlEnabled' => $this->swaggerUiContext->isGraphQlEnabled(),
'graphiQlEnabled' => $this->swaggerUiContext->isGraphiQlEnabled(),
'graphQlPlaygroundEnabled' => $this->swaggerUiContext->isGraphQlPlaygroundEnabled(),
];

$swaggerData = [
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
'spec' => $this->normalizer->normalize($openApi, 'json', []),
'oauth' => [
'enabled' => $this->openApiOptions->getOAuthEnabled(),
'type' => $this->openApiOptions->getOAuthType(),
'flow' => $this->openApiOptions->getOAuthFlow(),
'tokenUrl' => $this->openApiOptions->getOAuthTokenUrl(),
'authorizationUrl' => $this->openApiOptions->getOAuthAuthorizationUrl(),
'scopes' => $this->openApiOptions->getOAuthScopes(),
'clientId' => $this->oauthClientId,
'clientSecret' => $this->oauthClientSecret,
],
];

if ($request->isMethodSafe() && null !== $resourceClass = $request->attributes->get('_api_resource_class')) {
$swaggerData['id'] = $request->attributes->get('id');
$swaggerData['queryParameters'] = $request->query->all();

$metadata = $this->resourceMetadataFactory->create($resourceClass);
$swaggerData['shortName'] = $metadata->getShortName();

if (null !== $collectionOperationName = $request->attributes->get('_api_collection_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sCollection', $collectionOperationName, ucfirst($swaggerData['shortName']));
} elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sItem', $itemOperationName, ucfirst($swaggerData['shortName']));
} elseif (null !== $subresourceOperationContext = $request->attributes->get('_api_subresource_context')) {
$swaggerData['operationId'] = $subresourceOperationContext['operationId'];
}

[$swaggerData['path'], $swaggerData['method']] = $this->getPathAndMethod($swaggerData);
}

return new Response($this->twig->render('@ApiPlatform/SwaggerUi/index.html.twig', $swaggerContext + ['swagger_data' => $swaggerData]));
}

private function getPathAndMethod(array $swaggerData): array
{
foreach ($swaggerData['spec']['paths'] as $path => $operations) {
foreach ($operations as $method => $operation) {
if ($operation['operationId'] === $swaggerData['operationId']) {
return [$path, $method];
}
}
}

throw new RuntimeException(sprintf('The operation "%s" cannot be found in the Swagger specification.', $swaggerData['operationId']));
}
}
64 changes: 64 additions & 0 deletions src/Bridge/Symfony/Bundle/SwaggerUi/SwaggerUiContext.php
@@ -0,0 +1,64 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\SwaggerUi;

final class SwaggerUiContext
{
private $swaggerUiEnabled;
private $showWebby;
private $reDocEnabled;
private $graphQlEnabled;
private $graphiQlEnabled;
private $graphQlPlaygroundEnabled;

public function __construct(bool $swaggerUiEnabled = false, bool $showWebby = true, bool $reDocEnabled = false, bool $graphQlEnabled = false, bool $graphiQlEnabled = false, bool $graphQlPlaygroundEnabled = false)
{
$this->swaggerUiEnabled = $swaggerUiEnabled;
$this->showWebby = $showWebby;
$this->reDocEnabled = $reDocEnabled;
$this->graphQlEnabled = $graphQlEnabled;
$this->graphiQlEnabled = $graphiQlEnabled;
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
}

public function isSwaggerUiEnabled(): bool
{
return $this->swaggerUiEnabled;
}

public function isWebbyShown(): bool
{
return $this->showWebby;
}

public function isRedocEnabled(): bool
{
return $this->reDocEnabled;
}

public function isGraphQlEnabled(): bool
{
return $this->graphQlEnabled;
}

public function isGraphiQlEnabled(): bool
{
return $this->graphiQlEnabled;
}

public function isGraphQlPlaygroundEnabled(): bool
{
return $this->graphQlPlaygroundEnabled;
}
}
Expand Up @@ -1331,6 +1331,8 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
$definitions[] = 'api_platform.openapi.normalizer.api_gateway';
$definitions[] = 'api_platform.openapi.factory';
$definitions[] = 'api_platform.openapi.command';
$definitions[] = 'api_platform.swagger_ui.context';
$definitions[] = 'api_platform.swagger_ui.action';
}

// has jsonld
Expand Down Expand Up @@ -1405,6 +1407,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
Options::class => 'api_platform.openapi.options',
OpenApiNormalizer::class => 'api_platform.openapi.normalizer',
OpenApiFactoryInterface::class => 'api_platform.openapi.factory',
'api_platform.swagger_ui.listener' => 'api_platform.swagger.listener.ui',
];
}

Expand Down

0 comments on commit 36cd63c

Please sign in to comment.