Skip to content

Commit d1bca2c

Browse files
antograssiotdunglas
authored andcommitted
Correctly expose overridden formats in Swagger (#2317)
* Correctly expose reponse formats in Swagger * Rename the new interface and extends from the old one
1 parent c50e63e commit d1bca2c

File tree

6 files changed

+332
-10
lines changed

6 files changed

+332
-10
lines changed

src/Api/FormatsProvider.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*
2222
* @author Anthony GRASSIOT <[email protected]>
2323
*/
24-
final class FormatsProvider implements FormatsProviderInterface
24+
final class FormatsProvider implements FormatsProviderInterface, OperationAwareFormatsProviderInterface
2525
{
2626
private $configuredFormats;
2727
private $resourceMetadataFactory;
@@ -56,6 +56,26 @@ public function getFormatsFromAttributes(array $attributes): array
5656
return $this->getOperationFormats($formats);
5757
}
5858

59+
/**
60+
* {@inheritdoc}
61+
*
62+
* @throws InvalidArgumentException
63+
*/
64+
public function getFormatsFromOperation(string $resourceClass, string $operationName, string $operationType): array
65+
{
66+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
67+
68+
if (!$formats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'formats', [], true)) {
69+
return $this->configuredFormats;
70+
}
71+
72+
if (!\is_array($formats)) {
73+
throw new InvalidArgumentException(sprintf("The 'formats' attributes must be an array, %s given for resource class '%s'.", \gettype($formats), $resourceClass));
74+
}
75+
76+
return $this->getOperationFormats($formats);
77+
}
78+
5979
/**
6080
* Filter and populate the acceptable formats.
6181
*
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Api;
15+
16+
/**
17+
* Extracts formats for a given operation according to the retrieved Metadata.
18+
*
19+
* @author Anthony GRASSIOT <[email protected]>
20+
*/
21+
interface OperationAwareFormatsProviderInterface extends FormatsProviderInterface
22+
{
23+
/**
24+
* Finds formats for an operation.
25+
*/
26+
public function getFormatsFromOperation(string $resourceClass, string $operationName, string $operationType): array;
27+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>
2929
<argument>%api_platform.collection.pagination.client_items_per_page%</argument>
3030
<argument>%api_platform.collection.pagination.items_per_page_parameter_name%</argument>
31+
<argument type="service" id="api_platform.formats_provider" on-invalid="ignore" />
3132
<tag name="serializer.normalizer" priority="16" />
3233
</service>
3334

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Core\Api\FilterCollection;
1717
use ApiPlatform\Core\Api\FilterLocatorTrait;
18+
use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface;
1819
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
1920
use ApiPlatform\Core\Api\OperationType;
2021
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
@@ -68,11 +69,12 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup
6869
private $paginationPageParameterName;
6970
private $clientItemsPerPage;
7071
private $itemsPerPageParameterName;
72+
private $formatsProvider;
7173

7274
/**
7375
* @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
7476
*/
75-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage')
77+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage', OperationAwareFormatsProviderInterface $formatsProvider = null)
7678
{
7779
if ($urlGenerator) {
7880
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.1 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED);
@@ -99,6 +101,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
99101
$this->apiKeys = $apiKeys;
100102
$this->clientItemsPerPage = $clientItemsPerPage;
101103
$this->itemsPerPageParameterName = $itemsPerPageParameterName;
104+
$this->formatsProvider = $formatsProvider;
102105
}
103106

104107
/**
@@ -130,7 +133,11 @@ public function normalize($object, $format = null, array $context = [])
130133
$pathOperation = new \ArrayObject([]);
131134
$pathOperation['tags'] = $subresourceOperation['shortNames'];
132135
$pathOperation['operationId'] = $operationId;
133-
$pathOperation['produces'] = $mimeTypes;
136+
if (null !== $this->formatsProvider) {
137+
$responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
138+
$responseMimeTypes = $this->extractMimeTypes($responseFormats);
139+
}
140+
$pathOperation['produces'] = $responseMimeTypes ?? $mimeTypes;
134141
$pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
135142
$pathOperation['responses'] = [
136143
'200' => $subresourceOperation['collection'] ? [
@@ -223,17 +230,20 @@ private function getPathOperation(string $operationName, array $operation, strin
223230
if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
224231
$pathOperation['deprecated'] = true;
225232
}
226-
233+
if (null !== $this->formatsProvider) {
234+
$responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
235+
$responseMimeTypes = $this->extractMimeTypes($responseFormats);
236+
}
227237
switch ($method) {
228238
case 'GET':
229-
return $this->updateGetOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
239+
return $this->updateGetOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
230240
case 'POST':
231-
return $this->updatePostOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
241+
return $this->updatePostOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
232242
case 'PATCH':
233243
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
234244
// no break
235245
case 'PUT':
236-
return $this->updatePutOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
246+
return $this->updatePutOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
237247
case 'DELETE':
238248
return $this->updateDeleteOperation($pathOperation, $resourceShortName);
239249
}
@@ -678,4 +688,16 @@ private function getSerializerContext(string $operationType, bool $denormalizati
678688

679689
return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true);
680690
}
691+
692+
private function extractMimeTypes(array $responseFormats): array
693+
{
694+
$responseMimeTypes = [];
695+
foreach ($responseFormats as $mimeTypes) {
696+
foreach ($mimeTypes as $mimeType) {
697+
$responseMimeTypes[] = $mimeType;
698+
}
699+
}
700+
701+
return $responseMimeTypes;
702+
}
681703
}

tests/Api/FormatsProviderTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Core\Tests\Api;
1515

1616
use ApiPlatform\Core\Api\FormatsProvider;
17+
use ApiPlatform\Core\Api\OperationType;
1718
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1819
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
1920
use PHPUnit\Framework\TestCase;
@@ -106,4 +107,78 @@ public function testInvalidFormatsDeclaration()
106107

107108
$this->assertSame(['jsonld' => ['application/ld+json']], $formatProvider->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'get']));
108109
}
110+
111+
public function testResourceClassWithoutFormatsAttributesFromOperation()
112+
{
113+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
114+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata())->shouldBeCalled();
115+
116+
$formatProvider = new FormatsProvider($resourceMetadataFactoryProphecy->reveal(), ['jsonld' => ['application/ld+json']]);
117+
118+
$this->assertSame(['jsonld' => ['application/ld+json']], $formatProvider->getFormatsFromOperation('Foo', 'get', OperationType::COLLECTION));
119+
}
120+
121+
public function testResourceClassWithFormatsAttributesFromOperation()
122+
{
123+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
124+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => ['jsonld']]);
125+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
126+
127+
$formatProvider = new FormatsProvider($resourceMetadataFactoryProphecy->reveal(), ['jsonld' => ['application/ld+json'], 'json' => ['application/json']]);
128+
129+
$this->assertSame(['jsonld' => ['application/ld+json']], $formatProvider->getFormatsFromOperation('Foo', 'get', OperationType::COLLECTION));
130+
}
131+
132+
public function testResourceClassWithFormatsAttributesOverRiddingMimeTypesFromOperation()
133+
{
134+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
135+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => ['jsonld' => ['application/foo'], 'bar' => ['application/bar', 'application/baz'], 'buz' => 'application/fuz']]);
136+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
137+
138+
$formatProvider = new FormatsProvider($resourceMetadataFactoryProphecy->reveal(), ['jsonld' => ['application/ld+json'], 'json' => ['application/json']]);
139+
140+
$this->assertSame(['jsonld' => ['application/foo'], 'bar' => ['application/bar', 'application/baz'], 'buz' => ['application/fuz']], $formatProvider->getFormatsFromOperation('Foo', 'get', OperationType::COLLECTION));
141+
}
142+
143+
public function testBadFormatsShortDeclarationFromOperation()
144+
{
145+
$this->expectException(\ApiPlatform\Core\Exception\InvalidArgumentException::class);
146+
$this->expectExceptionMessage('You either need to add the format \'foo\' to your project configuration or declare a mime type for it in your annotation.');
147+
148+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
149+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => ['foo']]);
150+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
151+
152+
$formatProvider = new FormatsProvider($resourceMetadataFactoryProphecy->reveal(), ['jsonld' => ['application/ld+json']]);
153+
154+
$formatProvider->getFormatsFromOperation('Foo', 'get', OperationType::COLLECTION);
155+
}
156+
157+
public function testInvalidFormatsShortDeclarationFromOperation()
158+
{
159+
$this->expectException(\ApiPlatform\Core\Exception\InvalidArgumentException::class);
160+
$this->expectExceptionMessage('The \'formats\' attributes value must be a string when trying to include an already configured format, array given.');
161+
162+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
163+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => [['badFormat']]]);
164+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
165+
166+
$formatProvider = new FormatsProvider($resourceMetadataFactoryProphecy->reveal(), ['jsonld' => ['application/ld+json'], 'json' => ['application/json']]);
167+
168+
$this->assertSame(['jsonld' => ['application/ld+json']], $formatProvider->getFormatsFromOperation('Foo', 'get', OperationType::COLLECTION));
169+
}
170+
171+
public function testInvalidFormatsDeclarationFromOperation()
172+
{
173+
$this->expectException(\ApiPlatform\Core\Exception\InvalidArgumentException::class);
174+
$this->expectExceptionMessage('The \'formats\' attributes must be an array, string given for resource class \'Foo\'.');
175+
176+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
177+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => 'badFormat']);
178+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
179+
180+
$formatProvider = new FormatsProvider($resourceMetadataFactoryProphecy->reveal(), ['jsonld' => ['application/ld+json'], 'json' => ['application/json']]);
181+
182+
$this->assertSame(['jsonld' => ['application/ld+json']], $formatProvider->getFormatsFromOperation('Foo', 'get', OperationType::COLLECTION));
183+
}
109184
}

0 commit comments

Comments
 (0)