Skip to content

Commit f5f77d5

Browse files
authored
Merge pull request #2419 from antograssiot/openAPi-links
[OpenAPI] Add support for Links
2 parents 5dfc18e + 211fa33 commit f5f77d5

File tree

2 files changed

+63
-9
lines changed

2 files changed

+63
-9
lines changed

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,15 @@ public function normalize($object, $format = null, array $context = [])
135135
$mimeTypes = $object->getMimeTypes();
136136
$definitions = new \ArrayObject();
137137
$paths = new \ArrayObject();
138+
$links = new \ArrayObject();
138139

139140
foreach ($object->getResourceNameCollection() as $resourceClass) {
140141
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
141142
$resourceShortName = $resourceMetadata->getShortName();
142143

143-
$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
144-
$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
144+
// Items needs to be parsed first to be able to reference the lines from the collection operation
145+
$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM, $links);
146+
$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION, $links);
145147

146148
if (null === $this->subresourceOperationFactory) {
147149
continue;
@@ -236,7 +238,7 @@ public function normalize($object, $format = null, array $context = [])
236238
/**
237239
* Updates the list of entries in the paths collection.
238240
*/
239-
private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType)
241+
private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType, \ArrayObject $links)
240242
{
241243
if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
242244
return;
@@ -246,7 +248,7 @@ private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitio
246248
$path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
247249
$method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
248250

249-
$paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
251+
$paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions, $links);
250252
}
251253
}
252254

@@ -275,12 +277,15 @@ private function getPath(string $resourceShortName, string $operationName, array
275277
*
276278
* @param string[] $mimeTypes
277279
*/
278-
private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject
280+
private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
279281
{
280282
$pathOperation = new \ArrayObject($operation[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
281283
$resourceShortName = $resourceMetadata->getShortName();
282284
$pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
283285
$pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
286+
if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath($resourceShortName, $operationName, $operation, $operationType))) {
287+
$links[$pathOperation['operationId']] = $link;
288+
}
284289
if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
285290
$pathOperation['deprecated'] = true;
286291
}
@@ -292,7 +297,7 @@ private function getPathOperation(bool $v3, string $operationName, array $operat
292297
case 'GET':
293298
return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
294299
case 'POST':
295-
return $this->updatePostOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
300+
return $this->updatePostOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions, $links);
296301
case 'PATCH':
297302
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
298303
// no break
@@ -408,7 +413,7 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array
408413
return $pathOperation;
409414
}
410415

411-
private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
416+
private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
412417
{
413418
if (!$v3) {
414419
$pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
@@ -426,6 +431,9 @@ private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, arra
426431
if ($responseDefinitionKey) {
427432
if ($v3) {
428433
$successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
434+
if ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null) {
435+
$successResponse['links'] = [ucfirst($key) => $links[$key]];
436+
}
429437
} else {
430438
$successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
431439
}
@@ -852,4 +860,29 @@ private function extractMimeTypes(array $responseFormats): array
852860

853861
return $responseMimeTypes;
854862
}
863+
864+
/**
865+
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
866+
*/
867+
private function getLinkObject(string $resourceClass, string $operationId, string $path): array
868+
{
869+
$linkObject = $identifiers = [];
870+
foreach ($this->propertyNameCollectionFactory->create($resourceClass, []) as $propertyName) {
871+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
872+
if (!$propertyMetadata->isIdentifier()) {
873+
continue;
874+
}
875+
876+
$linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s', $propertyName);
877+
$identifiers[] = $propertyName;
878+
}
879+
880+
if (!$linkObject) {
881+
return [];
882+
}
883+
$linkObject['operationId'] = $operationId;
884+
$linkObject['description'] = 1 === \count($identifiers) ? sprintf('The `%1$s` value returned in the response can be used as the `%1$s` parameter in `GET %2$s`.', $identifiers[0], $path) : sprintf('The values returned in the response can be used in `GET %s`.', $path);
885+
886+
return $linkObject;
887+
}
855888
}

tests/Swagger/Serializer/DocumentationNormalizerV3Test.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function testNormalize()
6969
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
7070

7171
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
72-
$propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false));
72+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true));
7373
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, []));
7474
$propertyMetadataFactoryProphecy->create(Dummy::class, 'description')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is an initializable but not writable property.', true, false, true, true, false, false, null, null, [], null, true));
7575
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
@@ -180,6 +180,13 @@ public function testNormalize()
180180
'schema' => ['$ref' => '#/components/schemas/Dummy'],
181181
],
182182
],
183+
'links' => [
184+
'GetDummyItem' => [
185+
'operationId' => 'getDummyItem',
186+
'parameters' => ['id' => '$response.body#/id'],
187+
'description' => 'The `id` value returned in the response can be used as the `id` parameter in `GET /dummies/{id}`.',
188+
],
189+
],
183190
],
184191
'400' => ['description' => 'Invalid input'],
185192
'404' => ['description' => 'Resource not found'],
@@ -300,6 +307,13 @@ public function testNormalize()
300307
'schema' => ['$ref' => '#/components/schemas/Dummy'],
301308
],
302309
],
310+
'links' => [
311+
'GetDummyItem' => [
312+
'operationId' => 'getDummyItem',
313+
'parameters' => ['id' => '$response.body#/id'],
314+
'description' => 'The `id` value returned in the response can be used as the `id` parameter in `GET /dummies/{id}`.',
315+
],
316+
],
303317
],
304318
'400' => ['description' => 'Invalid input'],
305319
'404' => ['description' => 'Resource not found'],
@@ -2502,7 +2516,7 @@ public function testNormalizeWithCustomFormatsDefinedAtOperationLevel()
25022516
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
25032517

25042518
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
2505-
$propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false));
2519+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'id')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true));
25062520
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, []));
25072521
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
25082522
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
@@ -2609,6 +2623,13 @@ public function testNormalizeWithCustomFormatsDefinedAtOperationLevel()
26092623
'schema' => ['$ref' => '#/components/schemas/Dummy'],
26102624
],
26112625
],
2626+
'links' => [
2627+
'GetDummyItem' => [
2628+
'operationId' => 'getDummyItem',
2629+
'parameters' => ['id' => '$response.body#/id'],
2630+
'description' => 'The `id` value returned in the response can be used as the `id` parameter in `GET /dummies/{id}`.',
2631+
],
2632+
],
26122633
],
26132634
400 => ['description' => 'Invalid input'],
26142635
404 => ['description' => 'Resource not found'],

0 commit comments

Comments
 (0)