Skip to content

Commit

Permalink
fix(openapi): compatibility with OpenAPI 3.0 (#6065)
Browse files Browse the repository at this point in the history
fixes #5978

Co-authored-by: soyuka <[email protected]>
  • Loading branch information
Romaixn and soyuka authored Jan 10, 2024
1 parent af8726a commit 804da1b
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 7 deletions.
41 changes: 41 additions & 0 deletions features/openapi/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,44 @@ Feature: Documentation support
"$ref": "#\/components\/schemas\/WrappedResponseEntity-read"
}
"""

Scenario: Retrieve the OpenAPI documentation with 3.0 specification
Given I send a "GET" request to "/docs.jsonopenapi?spec_version=3.0.0"
Then the response status code should be 200
And the response should be in JSON
And the JSON node "openapi" should be equal to "3.0.0"
And the JSON node "components.schemas.DummyBoolean" should be equal to:
"""
{
"type": "object",
"description": "",
"deprecated": false,
"properties": {
"id": {
"readOnly": true,
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
},
"isDummyBoolean": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"dummyBoolean": {
"readOnly": true,
"type": "boolean"
}
}
}
"""
20 changes: 18 additions & 2 deletions src/Documentation/Action/DocumentationAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\OpenApi;
use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer;
use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer;
use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
Expand Down Expand Up @@ -60,7 +61,11 @@ public function __invoke(Request $request = null)
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
}

$context = ['api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), 'base_url' => $request->getBaseUrl()];
$context = [
'api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY),
'base_url' => $request->getBaseUrl(),
'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION),
];
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
$format = $this->getRequestFormat($request, $this->documentationFormats);

Expand All @@ -78,7 +83,18 @@ private function getOpenApiDocumentation(array $context, string $format, Request
{
if ($this->provider && $this->processor) {
$context['request'] = $request;
$operation = new Get(class: OpenApi::class, read: true, serialize: true, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null], outputFormats: $this->documentationFormats);
$operation = new Get(
class: OpenApi::class,
read: true,
serialize: true,
provider: fn () => $this->openApiFactory->__invoke($context),
normalizationContext: [
ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null,
LegacyOpenApiNormalizer::SPEC_VERSION => $context['spec_version'] ?? null,
],
outputFormats: $this->documentationFormats
);

if ('html' === $format) {
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
}
Expand Down
6 changes: 5 additions & 1 deletion src/Documentation/Action/EntrypointAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -41,7 +42,10 @@ public function __construct(
public function __invoke(Request $request)
{
static::$resourceNameCollection = $this->resourceNameCollectionFactory->create();
$context = ['request' => $request];
$context = [
'request' => $request,
'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION),
];
$request->attributes->set('_api_platform_disable_listeners', true);
$operation = new Get(outputFormats: $this->documentationFormats, read: true, serialize: true, class: Entrypoint::class, provider: [self::class, 'provide']);
$request->attributes->set('_api_operation', $operation);
Expand Down
4 changes: 3 additions & 1 deletion src/OpenApi/Command/OpenApiCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$filesystem = new Filesystem();
$io = new SymfonyStyle($input, $output);
$data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json');
$data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json', [
'spec_version' => $input->getOption('spec-version'),
]);
$content = $input->getOption('yaml')
? Yaml::dump($data, 10, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)
: (json_encode($data, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) ?: '');
Expand Down
67 changes: 67 additions & 0 deletions src/OpenApi/Serializer/LegacyOpenApiNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\OpenApi\Serializer;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class LegacyOpenApiNormalizer implements NormalizerInterface
{
public const SPEC_VERSION = 'spec_version';
private array $defaultContext = [
self::SPEC_VERSION => '3.1.0',
];

public function __construct(private readonly NormalizerInterface $decorated, $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}

public function normalize(mixed $object, string $format = null, array $context = []): array
{
$openapi = $this->decorated->normalize($object, $format, $context);

if ('3.0.0' !== ($context['spec_version'] ?? null)) {
return $openapi;
}

$schemas = &$openapi['components']['schemas'];
$openapi['openapi'] = '3.0.0';
foreach ($openapi['components']['schemas'] as $name => $component) {
foreach ($component['properties'] ?? [] as $property => $value) {
if (\is_array($value['type'] ?? false)) {
foreach ($value['type'] as $type) {
$schemas[$name]['properties'][$property]['anyOf'][] = ['type' => $type];
}
unset($schemas[$name]['properties'][$property]['type']);
}
unset($schemas[$name]['properties'][$property]['owl:maxCardinality']);
}
}

return $openapi;
}

/**
* {@inheritdoc}
*/
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return $this->decorated->supportsNormalization($data, $format, $context);
}

public function getSupportedTypes($format): array
{
return $this->decorated->getSupportedTypes($format);
}
}
7 changes: 6 additions & 1 deletion src/Symfony/Bundle/Resources/config/openapi.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@

<service id="api_platform.openapi.normalizer.api_gateway" class="ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer" public="false" decorates="api_platform.openapi.normalizer" decoration-priority="-1">
<argument type="service" id="api_platform.openapi.normalizer.api_gateway.inner" />
<tag name="serializer.normalizer" priority="-780" />
<tag name="serializer.normalizer" />
</service>

<service id="api_platform.openapi.normalizer.legacy" class="ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer" public="false" decorates="api_platform.openapi.normalizer.api_gateway" decoration-priority="-2">
<argument type="service" id="api_platform.openapi.normalizer.legacy.inner" />
<tag name="serializer.normalizer" />
</service>
<service id="ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface" alias="api_platform.openapi.factory" />

Expand Down
6 changes: 4 additions & 2 deletions tests/Documentation/Action/DocumentationActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ public function testDocumentationAction(): void
$requestProphecy->headers = $this->prophesize(ParameterBagInterface::class)->reveal();
$requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1);
$queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1);
$queryProphecy->get('spec_version')->willReturn('3.1.0')->shouldBeCalledTimes(1);
$attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1);
$attributesProphecy->get('_format')->willReturn(null)->shouldBeCalledTimes(1);
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true])->shouldBeCalledTimes(1);
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0'])->shouldBeCalledTimes(1);

$documentation = new DocumentationAction($this->prophesize(ResourceNameCollectionFactoryInterface::class)->reveal(), 'my api', '', '1.0.0', $openApiFactoryProphecy->reveal());
$this->assertInstanceOf(OpenApi::class, $documentation($requestProphecy->reveal()));
Expand All @@ -75,8 +76,9 @@ public function testDocumentationActionWithoutOpenApiFactory(): void
$requestProphecy->query = $queryProphecy->reveal();
$requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1);
$queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1);
$queryProphecy->get('spec_version')->willReturn('3.1.0')->shouldBeCalledTimes(1);
$attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1);
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true])->shouldBeCalledTimes(1);
$attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0'])->shouldBeCalledTimes(1);
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies']))->shouldBeCalled();

Expand Down

0 comments on commit 804da1b

Please sign in to comment.