diff --git a/features/bootstrap/SwaggerContext.php b/features/bootstrap/SwaggerContext.php index 61c333788c6..6e557c81302 100644 --- a/features/bootstrap/SwaggerContext.php +++ b/features/bootstrap/SwaggerContext.php @@ -176,9 +176,18 @@ private function getProperties(string $className, int $specVersion = 2): stdClas private function getClassInfo(string $className, int $specVersion = 2): stdClass { $nodes = 2 === $specVersion ? $this->getLastJsonResponse()->{'definitions'} : $this->getLastJsonResponse()->{'components'}->{'schemas'}; - foreach ($nodes as $classTitle => $classData) { - if ($classTitle === $className) { - return $classData; + + if (3 === $specVersion) { + foreach ($nodes as $classTitle => $classData) { + if (substr($classTitle, -strlen($className)) === $className) { + return $classData; + } + } + } else { + foreach ($nodes as $classTitle => $classData) { + if ($classTitle === $className) { + return $classData; + } } } diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 0c8eb4dd50b..baac73155db 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -95,7 +95,7 @@ Feature: Documentation support And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 6 elements # Subcollection - check schema - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.items.$ref" should be equal to "#/components/schemas/RelatedToDummyFriend-fakemanytomany" + And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.items.$ref" should be equal to "#/components/schemas/application-ld+json-RelatedToDummyFriend-fakemanytomany" # Deprecations And the JSON node "paths./dummies.get.deprecated" should not exist diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 768cc4ab6fb..9d374a50d80 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -32,6 +32,7 @@ use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface; use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface; use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; +use ApiPlatform\Core\Swagger\SchemaFormatter\DefinititionNormalizerInterface; use Doctrine\Common\Annotations\Annotation; use phpDocumentor\Reflection\DocBlockFactoryInterface; use Ramsey\Uuid\Uuid; @@ -321,6 +322,9 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_swagger', $config['enable_swagger']); $container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']); + + $container->registerForAutoconfiguration(DefinititionNormalizerInterface::class) + ->addTag('api_platform.schema_formatter'); } private function registerJsonApiConfiguration(array $formats, XmlFileLoader $loader): void diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml index 9b2539356bc..90d8a8944ae 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml @@ -31,7 +31,10 @@ %api_platform.formats% %api_platform.collection.pagination.client_enabled% %api_platform.collection.pagination.enabled_parameter_name% + + + @@ -48,6 +51,20 @@ + + + + + + + + + + + + + + diff --git a/src/Exception/FormatterNotFoundException.php b/src/Exception/FormatterNotFoundException.php new file mode 100644 index 00000000000..2a856637373 --- /dev/null +++ b/src/Exception/FormatterNotFoundException.php @@ -0,0 +1,18 @@ + + * + * 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\Exception; + +final class FormatterNotFoundException extends InvalidArgumentException +{ +} diff --git a/src/Swagger/SchemaFormatter/ChainSchemaFormatter.php b/src/Swagger/SchemaFormatter/ChainSchemaFormatter.php new file mode 100644 index 00000000000..d3d9d8eb877 --- /dev/null +++ b/src/Swagger/SchemaFormatter/ChainSchemaFormatter.php @@ -0,0 +1,44 @@ + + * + * 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\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Exception\FormatterNotFoundException; + +final class ChainSchemaFormatter implements SchemaFormatterInterface +{ + private $formatters; + + /** + * SchemaFormatterProvider constructor. + * + * @param DefinititionNormalizerInterface[] $formatters + */ + public function __construct(/* iterable */ $formatters) + { + $this->formatters = $formatters; + } + + public function getFormatter(string $mimeType): DefinititionNormalizerInterface + { + foreach ($this->formatters as $formatter) { + if ($formatter->supports($mimeType)) { + return $formatter; + } + } + + throw new FormatterNotFoundException( + sprintf('No formatter supporting the "%s" MIME type is available.', $mimeType) + ); + } +} diff --git a/src/Swagger/SchemaFormatter/DefaultDefinitionNormalizer.php b/src/Swagger/SchemaFormatter/DefaultDefinitionNormalizer.php new file mode 100644 index 00000000000..b4cdee516a8 --- /dev/null +++ b/src/Swagger/SchemaFormatter/DefaultDefinitionNormalizer.php @@ -0,0 +1,34 @@ + + * + * 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\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; + +class DefaultDefinitionNormalizer implements DefinititionNormalizerInterface +{ + public function supports(string $mimeType): bool + { + return true; + } + + public function buildBaseSchemaFormat(): array + { + return []; + } + + public function setProperty(\ArrayObject $definitionSchema, $normalizedPropertyName, \ArrayObject $property, PropertyMetadata $propertyMetadata): void + { + $definitionSchema['properties'][$normalizedPropertyName] = $property; + } +} diff --git a/src/Swagger/SchemaFormatter/DefinititionNormalizerInterface.php b/src/Swagger/SchemaFormatter/DefinititionNormalizerInterface.php new file mode 100644 index 00000000000..c668ecc5e52 --- /dev/null +++ b/src/Swagger/SchemaFormatter/DefinititionNormalizerInterface.php @@ -0,0 +1,34 @@ + + * + * 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\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; + +interface DefinititionNormalizerInterface +{ + /** + * Returns if the mimetype is supported by this formatter. + */ + public function supports(string $mimeType): bool; + + /** + * Builds the basic schema if needed for this mimetype. + */ + public function buildBaseSchemaFormat(): array; + + /** + * Sets the property in the correct fields for this mime type. + */ + public function setProperty(\ArrayObject $definitionSchema, $normalizedPropertyName, \ArrayObject $property, PropertyMetadata $propertyMetadata): void; +} diff --git a/src/Swagger/SchemaFormatter/HydraDefinitionNormalizer.php b/src/Swagger/SchemaFormatter/HydraDefinitionNormalizer.php new file mode 100644 index 00000000000..e25be7d7cdb --- /dev/null +++ b/src/Swagger/SchemaFormatter/HydraDefinitionNormalizer.php @@ -0,0 +1,65 @@ + + * + * 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\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; + +final class HydraDefinitionNormalizer implements DefinititionNormalizerInterface +{ + public function supports(string $mimeType): bool + { + return 'application/ld+json' === $mimeType; + } + + public function buildBaseSchemaFormat(): array + { + return [ + 'hydra:member' => [ + 'type' => 'object', + 'properties' => [ + '@type' => [ + 'type' => 'string', + ], + '@id' => [ + 'type' => 'integer', + ], + ], + ], + ]; + } + + public function setProperty(\ArrayObject $definitionSchema, $normalizedPropertyName, \ArrayObject $property, PropertyMetadata $propertyMetadata): void + { + if (null !== $propertyMetadata->getType() + && 'object' === $propertyMetadata->getType()->getBuiltinType() + && isset($property['$ref']) + ) { + // @todo: Fix one to many statement. +// if (false) { +// $data = [ +// 'type' => 'object', +// 'properties' => [ +// $data, +// ], +// ]; +// } + + $definitionSchema['properties']['hydra:member']['properties'][$normalizedPropertyName] = $property; + + return; + } + + $definitionSchema['properties']['hydra:member']['properties'][$normalizedPropertyName] = $property; + } +} diff --git a/src/Swagger/SchemaFormatter/JsonApiDefinitionNormalizer.php b/src/Swagger/SchemaFormatter/JsonApiDefinitionNormalizer.php new file mode 100644 index 00000000000..634ccf064d9 --- /dev/null +++ b/src/Swagger/SchemaFormatter/JsonApiDefinitionNormalizer.php @@ -0,0 +1,106 @@ + + * + * 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\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; + +final class JsonApiDefinitionNormalizer implements DefinititionNormalizerInterface +{ + public function supports(string $mimeType): bool + { + return 'application/vnd.api+json' === $mimeType; + } + + public function buildBaseSchemaFormat(): array + { + return [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'integer', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [], + ], + ], + ], + ]; + } + + public function setProperty(\ArrayObject $definitionSchema, $normalizedPropertyName, \ArrayObject $property, PropertyMetadata $propertyMetadata): void + { + if ('id' === $normalizedPropertyName) { + $definitionSchema['properties']['data']['properties'][$normalizedPropertyName] = $property; + $normalizedPropertyName = '_id'; + } + + if (null !== $propertyMetadata->getType() + && 'object' === $propertyMetadata->getType()->getBuiltinType() + && isset($property['$ref']) + ) { + $data = [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'string', + ], + ], + ]; + // @todo: Fix one to many statement. +// if (false) { +// $data = [ +// 'type' => 'object', +// 'properties' => [ +// $data, +// ], +// ]; +// } + + $definitionSchema['properties']['data']['properties']['relationships'] = [ + 'type' => 'object', + 'properties' => [ + $normalizedPropertyName => [ + 'type' => 'object', + 'properties' => [ + 'links' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'string', + ], + 'related' => [ + 'type' => 'string', + ], + ], + ], + 'data' => $data, + ], + ], + ], + ]; + + return; + } + + $definitionSchema['properties']['data']['properties']['attributes']['properties'][$normalizedPropertyName] = $property; + } +} diff --git a/src/Swagger/SchemaFormatter/SchemaFormatterInterface.php b/src/Swagger/SchemaFormatter/SchemaFormatterInterface.php new file mode 100644 index 00000000000..93bec80180c --- /dev/null +++ b/src/Swagger/SchemaFormatter/SchemaFormatterInterface.php @@ -0,0 +1,18 @@ + + * + * 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\Swagger\SchemaFormatter; + +interface SchemaFormatterInterface +{ +} diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php index 5105ab5f204..187c8c7b06c 100644 --- a/src/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Swagger/Serializer/DocumentationNormalizer.php @@ -30,6 +30,7 @@ use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; +use ApiPlatform\Core\Swagger\SchemaFormatter\SchemaFormatterInterface; use Psr\Container\ContainerInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -89,12 +90,13 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup self::SPEC_VERSION => 2, ApiGatewayNormalizer::API_GATEWAY => false, ]; + private $schemaFormatter; /** * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection * @param array|OperationAwareFormatsProviderInterface $formats */ - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = []) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], SchemaFormatterInterface $schemaFormatter = null) { if ($urlGenerator) { @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); @@ -136,6 +138,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName; $this->defaultContext = array_merge($this->defaultContext, $defaultContext); + $this->schemaFormatter = $schemaFormatter; } /** @@ -260,9 +263,17 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName); $responseDefinitionKey = false; + $responseDefinitionKeys = []; $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true); if (null !== $outputClass = $outputMetadata['class'] ?? null) { - $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $serializerContext); + if ($v3) { + foreach ($mimeTypes as $mimeType) { + $responseDefinitionKeys[$mimeType] = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, + $outputClass, $serializerContext, $mimeType, true); + } + } else { + $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $serializerContext); + } } $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200'); @@ -275,16 +286,21 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName); $successResponse = ['description' => sprintf('%s collection response', $resourceShortName)]; - - if ($responseDefinitionKey) { - if ($v3) { - $successResponse['content'] = array_fill_keys($mimeTypes, [ - 'schema' => [ - 'type' => 'array', - 'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)], - ], - ]); - } else { + if ($v3) { + if (!empty($responseDefinitionKeys)) { + foreach ($mimeTypes as $mimeType) { + $successResponse['content'][$mimeType] = [ + 'schema' => [ + 'type' => 'array', + 'items' => [ + '$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKeys[$mimeType]), + ], + ], + ]; + } + } + } else { + if ($responseDefinitionKey) { $successResponse['schema'] = [ 'type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)], @@ -305,10 +321,19 @@ private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $pathOperation = $this->addItemOperationParameters($v3, $pathOperation); $successResponse = ['description' => sprintf('%s resource response', $resourceShortName)]; - if ($responseDefinitionKey) { - if ($v3) { - $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]); - } else { + + if ($v3) { + if (!empty($responseDefinitionKeys)) { + foreach ($mimeTypes as $mimeType) { + $successResponse['content'][$mimeType] = [ + 'schema' => [ + '$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKeys[$mimeType]), + ], + ]; + } + } + } else { + if ($responseDefinitionKey) { $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]; } } @@ -367,7 +392,7 @@ private function addSubresourceOperation(bool $v3, array $subresourceOperation, $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']); $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName); - $responseDefinitionKey = $this->getDefinition($v3, $definitions, $subResourceMetadata, $subresourceOperation['resource_class'], null, $serializerContext); + $pathOperation = new \ArrayObject([]); $pathOperation['tags'] = $subresourceOperation['shortNames']; $pathOperation['operationId'] = $operationId; @@ -390,7 +415,19 @@ private function addSubresourceOperation(bool $v3, array $subresourceOperation, $pathOperation['produces'] = $mimeTypes; } - $pathOperation['responses'] = $this->getSubresourceResponse($v3, $mimeTypes, $subresourceOperation['collection'], $subresourceOperation['shortNames'][0], $responseDefinitionKey); + $responseDefinitionKeys = []; + $responseDefinitionKey = ''; + if ($v3) { + foreach ($responseMimeTypes ?? $mimeTypes as $mimeType) { + $responseDefinitionKeys[$mimeType] = $this->getDefinition($v3, $definitions, $subResourceMetadata, + $subresourceOperation['resource_class'], null, $serializerContext, $mimeType); + } + } else { + $responseDefinitionKey = $this->getDefinition($v3, $definitions, $subResourceMetadata, + $subresourceOperation['resource_class'], null, $serializerContext); + } + + $pathOperation['responses'] = $this->getSubresourceResponse($v3, $mimeTypes, $subresourceOperation['collection'], $subresourceOperation['shortNames'][0], $responseDefinitionKey, $responseDefinitionKeys); // Avoid duplicates parameters when there is a filter on a subresource identifier $parametersMemory = []; $pathOperation['parameters'] = []; @@ -417,7 +454,7 @@ private function addSubresourceOperation(bool $v3, array $subresourceOperation, return new \ArrayObject(['get' => $pathOperation]); } - private function getSubresourceResponse(bool $v3, $mimeTypes, bool $collection, string $shortName, string $definitionKey): array + private function getSubresourceResponse(bool $v3, $mimeTypes, bool $collection, string $shortName, string $definitionKey, array $definitionKeys = []): array { if ($collection) { $okResponse = [ @@ -425,7 +462,9 @@ private function getSubresourceResponse(bool $v3, $mimeTypes, bool $collection, ]; if ($v3) { - $okResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/components/schemas/%s', $definitionKey)]]]); + foreach ($mimeTypes as $mimeType) { + $okResponse['content'][$mimeType] = ['schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/components/schemas/%s', $definitionKeys[$mimeType])]]]; + } } else { $okResponse['schema'] = ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $definitionKey)]]; } @@ -435,7 +474,16 @@ private function getSubresourceResponse(bool $v3, $mimeTypes, bool $collection, ]; if ($v3) { - $okResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $definitionKey)]]); + foreach ($mimeTypes as $mimeType) { + $okResponse['content'][$mimeType] = [ + 'schema' => [ + '$ref' => sprintf( + '#/components/schemas/%s', + $definitionKeys[$mimeType] + ), + ], + ]; + } } else { $okResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $definitionKey)]; } @@ -459,19 +507,40 @@ private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, arra } $responseDefinitionKey = false; + $responseDefinitionKeys = []; $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true); if (null !== $outputClass = $outputMetadata['class'] ?? null) { - $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)); + if ($v3) { + foreach ($responseMimeTypes as $mimeType) { + $responseDefinitionKeys[$mimeType] = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, + $outputClass, + $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName), $mimeType, true); + } + } else { + $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, + $outputClass, + $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)); + } } $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)]; - if ($responseDefinitionKey) { - if ($v3) { - $successResponse['content'] = array_fill_keys($responseMimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]); + + if ($v3) { + if (!empty($responseDefinitionKeys)) { + foreach ($responseMimeTypes as $mimeType) { + $successResponse['content'][$mimeType] = [ + 'schema' => [ + '$ref' => sprintf('#/components/schemas/%s', + $responseDefinitionKeys[$mimeType]), + ], + ]; + } if ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null) { $successResponse['links'] = [ucfirst($key) => $links[$key]]; } - } else { + } + } else { + if ($responseDefinitionKey) { $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]; } } @@ -487,19 +556,24 @@ private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, arra return $pathOperation; } - $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)); if ($v3) { + $content = []; + foreach ($responseMimeTypes as $mimeType) { + $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName), $mimeType, true); + $content[$mimeType] = ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]; + } $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [ - 'content' => array_fill_keys($requestMimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]), + 'content' => $content, 'description' => sprintf('The new %s resource', $resourceShortName), ]; } else { - $userDefinedParameters ?? $pathOperation['parameters'][] = [ + $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)); + $userDefinedParameters ?? $pathOperation['parameters'] = [[ 'name' => lcfirst($resourceShortName), 'in' => 'body', 'description' => sprintf('The new %s resource', $resourceShortName), 'schema' => ['$ref' => sprintf('#/definitions/%s', $requestDefinitionKey)], - ]; + ]]; } return $pathOperation; @@ -517,16 +591,40 @@ private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $pathOperation = $this->addItemOperationParameters($v3, $pathOperation); $responseDefinitionKey = false; + $responseDefinitionKeys = []; $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true); if (null !== $outputClass = $outputMetadata['class'] ?? null) { - $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)); + if ($v3) { + foreach ($responseMimeTypes as $mimeType) { + $responseDefinitionKeys[$mimeType] = $this->getDefinition($v3, $definitions, $resourceMetadata, + $resourceClass, + $outputClass, + $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName), + $mimeType, + true); + } + } else { + $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, + $outputClass, + $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)); + } } $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)]; - if ($responseDefinitionKey) { - if ($v3) { - $successResponse['content'] = array_fill_keys($responseMimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]); - } else { + + if ($v3) { + if (!empty($responseDefinitionKeys)) { + foreach ($responseMimeTypes as $mimeType) { + $successResponse['content'][$mimeType] = [ + 'schema' => [ + '$ref' => sprintf('#/components/schemas/%s', + $responseDefinitionKeys[$mimeType]), + ], + ]; + } + } + } else { + if ($responseDefinitionKey) { $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]; } } @@ -542,13 +640,18 @@ private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array return $pathOperation; } - $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)); if ($v3) { + $content = []; + foreach ($responseMimeTypes as $mimeType) { + $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName), $mimeType, true); + $content[$mimeType] = ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]; + } $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [ - 'content' => array_fill_keys($requestMimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]), + 'content' => $content, 'description' => sprintf('The updated %s resource', $resourceShortName), ]; } else { + $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)); $pathOperation['parameters'][] = [ 'name' => lcfirst($resourceShortName), 'in' => 'body', @@ -584,7 +687,7 @@ private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperatio return $pathOperation; } - private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, ?string $publicClass, array $serializerContext = null): string + private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, ?string $publicClass, array $serializerContext = null, $mimeType = null, bool $urlencode = false): string { $keyPrefix = $resourceMetadata->getShortName(); if (null !== $publicClass && $resourceClass !== $publicClass) { @@ -597,9 +700,23 @@ private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMeta $definitionKey = $this->getDefinitionKey($keyPrefix, (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? [])); } - if (!isset($definitions[$definitionKey])) { - $definitions[$definitionKey] = []; // Initialize first to prevent infinite loop - $definitions[$definitionKey] = $this->getDefinitionSchema($v3, $publicClass ?? $resourceClass, $resourceMetadata, $definitions, $serializerContext); + if ($v3) { + $definitionKey = str_replace('/', '-', $mimeType).'-'.$definitionKey; + if (!isset($definitions[$definitionKey])) { + $definitions[$definitionKey] = []; + $definitions[$definitionKey] = $this->getDefinitionSchema($v3, + $publicClass ?? $resourceClass, $resourceMetadata, $definitions, $serializerContext, $mimeType); + } + } else { + if (!isset($definitions[$definitionKey])) { + $definitions[$definitionKey] = []; // Initialize first to prevent infinite loop + $definitions[$definitionKey] = $this->getDefinitionSchema($v3, $publicClass ?? $resourceClass, + $resourceMetadata, $definitions, $serializerContext); + } + } + + if ($urlencode) { + return urlencode($definitionKey); } return $definitionKey; @@ -614,8 +731,10 @@ private function getDefinitionKey(string $resourceShortName, array $groups): str * Gets a definition Schema Object. * * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject + * + * @param mixed|null $mimeType */ - private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject + private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null, $mimeType = null): \ArrayObject { $definitionSchema = new \ArrayObject(['type' => 'object']); @@ -628,6 +747,14 @@ private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMe } $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : []; + + $schemaFormatter = null; + if ($v3) { + $schemaFormatter = $this->schemaFormatter->getFormatter($mimeType); + + $definitionSchema['properties'] = $schemaFormatter->buildBaseSchemaFormat(); + } + foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) { @@ -639,7 +766,11 @@ private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMe $definitionSchema['required'][] = $normalizedPropertyName; } - $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($v3, $propertyMetadata, $definitions, $serializerContext); + if ($v3 && null !== $schemaFormatter) { + $schemaFormatter->setProperty($definitionSchema, $normalizedPropertyName, $this->getPropertySchema($v3, $propertyMetadata, $definitions, $serializerContext, $mimeType), $propertyMetadata); + } else { + $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($v3, $propertyMetadata, $definitions, $serializerContext); + } } return $definitionSchema; @@ -650,7 +781,7 @@ private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMe * * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject */ - private function getPropertySchema(bool $v3, PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject + private function getPropertySchema(bool $v3, PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null, string $mimeType = null): \ArrayObject { $propertySchema = new \ArrayObject($propertyMetadata->getAttributes()[$v3 ? 'openapi_context' : 'swagger_context'] ?? []); @@ -675,7 +806,7 @@ private function getPropertySchema(bool $v3, PropertyMetadata $propertyMetadata, $className = $valueType->getClassName(); } - $valueSchema = $this->getType($v3, $builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext); + $valueSchema = $this->getType($v3, $builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext, $mimeType); return new \ArrayObject((array) $propertySchema + $valueSchema); } @@ -683,10 +814,10 @@ private function getPropertySchema(bool $v3, PropertyMetadata $propertyMetadata, /** * Gets the Swagger's type corresponding to the given PHP's type. */ - private function getType(bool $v3, string $type, bool $isCollection, ?string $className, ?bool $readableLink, \ArrayObject $definitions, array $serializerContext = null): array + private function getType(bool $v3, string $type, bool $isCollection, ?string $className, ?bool $readableLink, \ArrayObject $definitions, array $serializerContext = null, string $mimeType = null): array { if ($isCollection) { - return ['type' => 'array', 'items' => $this->getType($v3, $type, false, $className, $readableLink, $definitions, $serializerContext)]; + return ['type' => 'array', 'items' => $this->getType($v3, $type, false, $className, $readableLink, $definitions, $serializerContext, $mimeType)]; } if (Type::BUILTIN_TYPE_STRING === $type) { @@ -719,9 +850,18 @@ private function getType(bool $v3, string $type, bool $isCollection, ?string $cl } if (true === $readableLink) { + if ($v3) { + return [ + '$ref' => sprintf( + '#/components/schemas/%s', + $this->getDefinition($v3, $definitions, $resourceMetadata = $this->resourceMetadataFactory->create($className), $className, $resourceMetadata->getAttribute('output')['class'] ?? $className, $serializerContext, $mimeType, true) + ), + ]; + } + return [ '$ref' => sprintf( - $v3 ? '#/components/schemas/%s' : '#/definitions/%s', + '#/definitions/%s', $this->getDefinition($v3, $definitions, $resourceMetadata = $this->resourceMetadataFactory->create($className), $className, $resourceMetadata->getAttribute('output')['class'] ?? $className, $serializerContext) ), ]; diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index e3b5c5b9af4..75cc68112d5 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -73,6 +73,7 @@ use ApiPlatform\Core\Serializer\Filter\GroupFilter; use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Core\Swagger\SchemaFormatter\DefinititionNormalizerInterface; use ApiPlatform\Core\Tests\Fixtures\TestBundle\TestBundle; use ApiPlatform\Core\Validator\ValidatorInterface; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; @@ -1034,6 +1035,10 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); $this->childDefinitionProphecy->addTag('api_platform.data_transformer')->shouldBeCalledTimes(1); + $containerBuilderProphecy->registerForAutoconfiguration(DefinititionNormalizerInterface::class) + ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); + $this->childDefinitionProphecy->addTag('api_platform.schema_formatter')->shouldBeCalledTimes(1); + $containerBuilderProphecy->addResource(Argument::type(DirectoryResource::class))->shouldBeCalled(); $parameters = [ @@ -1162,6 +1167,9 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.swagger.normalizer.documentation', 'api_platform.validator', 'test.api_platform.client', + 'api_platform.schema_formatter.json', + 'api_platform.schema_formatter.api+json', + 'api_platform.schema_formatter.provider', ]; if (\in_array('odm', $doctrineIntegrationsToLoad, true)) { diff --git a/tests/Swagger/SchemaFormatter/ChainSchemaFormatterTest.php b/tests/Swagger/SchemaFormatter/ChainSchemaFormatterTest.php new file mode 100644 index 00000000000..d3e4d478a92 --- /dev/null +++ b/tests/Swagger/SchemaFormatter/ChainSchemaFormatterTest.php @@ -0,0 +1,55 @@ + + * + * 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\Tests\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Exception\FormatterNotFoundException; +use ApiPlatform\Core\Swagger\SchemaFormatter\ChainSchemaFormatter; +use ApiPlatform\Core\Swagger\SchemaFormatter\DefaultDefinitionNormalizer; +use ApiPlatform\Core\Swagger\SchemaFormatter\HydraDefinitionNormalizer; +use ApiPlatform\Core\Swagger\SchemaFormatter\JsonApiDefinitionNormalizer; +use PHPUnit\Framework\TestCase; + +class ChainSchemaFormatterTest extends TestCase +{ + public function testGetFormatter() + { + $schemaFormatterFactory = new ChainSchemaFormatter([ + new HydraDefinitionNormalizer(), + new JsonApiDefinitionNormalizer(), + new DefaultDefinitionNormalizer(), + ]); + $formatter = $schemaFormatterFactory->getFormatter('application/json'); + $this->assertInstanceOf(DefaultDefinitionNormalizer::class, $formatter); + } + + public function testGetFormatterDefault() + { + $schemaFormatterFactory = new ChainSchemaFormatter([ + new HydraDefinitionNormalizer(), + new JsonApiDefinitionNormalizer(), + new DefaultDefinitionNormalizer(), + ]); + + $formatter = $schemaFormatterFactory->getFormatter('application/json-test'); + $this->assertInstanceOf(DefaultDefinitionNormalizer::class, $formatter); + } + + public function testGetFormatterExceptionNoFormatters() + { + $schemaFormatterFactory = new ChainSchemaFormatter([]); + + $this->expectException(FormatterNotFoundException::class); + $schemaFormatterFactory->getFormatter('application/json-test'); + } +} diff --git a/tests/Swagger/SchemaFormatter/DefaultSchemaFormatterTest.php b/tests/Swagger/SchemaFormatter/DefaultSchemaFormatterTest.php new file mode 100644 index 00000000000..2ce27b439d9 --- /dev/null +++ b/tests/Swagger/SchemaFormatter/DefaultSchemaFormatterTest.php @@ -0,0 +1,64 @@ + + * + * 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\Tests\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Swagger\SchemaFormatter\DefaultDefinitionNormalizer; +use PHPUnit\Framework\TestCase; + +class DefaultSchemaFormatterTest extends TestCase +{ + public function testSupports() + { + $schemaFormatter = new DefaultDefinitionNormalizer(); + $this->assertTrue($schemaFormatter->supports('application/json')); + } + + public function testSupportsNotSupported() + { + $schemaFormatter = new DefaultDefinitionNormalizer(); + $this->assertTrue($schemaFormatter->supports('application/jso')); + } + + public function testBuildBaseSchemaFormat() + { + $schemaFormatter = new DefaultDefinitionNormalizer(); + $this->assertEquals([], $schemaFormatter->buildBaseSchemaFormat()); + } + + public function testSetProperty() + { + $schemaFormatter = new DefaultDefinitionNormalizer(); + $definitionSchema = new \ArrayObject([]); + $normalizedPropertyName = 'test'; + $property = new \ArrayObject([ + 'test' => 'foo', + ]); + $propertyMetadata = new PropertyMetadata(); + + $definitionSchema['properties'] = $schemaFormatter->buildBaseSchemaFormat(); + $schemaFormatter->setProperty($definitionSchema, $normalizedPropertyName, $property, $propertyMetadata); + + $this->assertEquals( + new \ArrayObject([ + 'properties' => [ + 'test' => new \ArrayObject([ + 'test' => 'foo', + ]), + ], + ]), + $definitionSchema + ); + } +} diff --git a/tests/Swagger/SchemaFormatter/JsonApiSchemaFormatterTest.php b/tests/Swagger/SchemaFormatter/JsonApiSchemaFormatterTest.php new file mode 100644 index 00000000000..80294ba9ca8 --- /dev/null +++ b/tests/Swagger/SchemaFormatter/JsonApiSchemaFormatterTest.php @@ -0,0 +1,99 @@ + + * + * 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\Tests\Swagger\SchemaFormatter; + +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Swagger\SchemaFormatter\JsonApiDefinitionNormalizer; +use PHPUnit\Framework\TestCase; + +class JsonApiSchemaFormatterTest extends TestCase +{ + public function testSupports() + { + $schemaFormatter = new JsonApiDefinitionNormalizer(); + $this->assertTrue($schemaFormatter->supports('application/vnd.api+json')); + } + + public function testSupportsNotSupported() + { + $schemaFormatter = new JsonApiDefinitionNormalizer(); + $this->assertFalse($schemaFormatter->supports('application/jso')); + } + + public function testBuildBaseSchemaFormat() + { + $schemaFormatter = new JsonApiDefinitionNormalizer(); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'integer', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [], + ], + ], + ], + ], + $schemaFormatter->buildBaseSchemaFormat() + ); + } + + public function testSetProperty() + { + $schemaFormatter = new JsonApiDefinitionNormalizer(); + $definitionSchema = new \ArrayObject([]); + $normalizedPropertyName = 'test'; + $property = new \ArrayObject([ + 'test' => 'foo', + ]); + $propertyMetadata = new PropertyMetadata(); + + $definitionSchema['properties'] = $schemaFormatter->buildBaseSchemaFormat(); + $schemaFormatter->setProperty($definitionSchema, $normalizedPropertyName, $property, $propertyMetadata); + + $this->assertEquals( + new \ArrayObject([ + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'integer', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + 'test' => new \ArrayObject([ + 'test' => 'foo', + ]), + ], + ], + ], + ], + ], + ]), + $definitionSchema + ); + } +} diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php index d7ab92d2d52..14aec48e834 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -34,6 +34,10 @@ use ApiPlatform\Core\PathResolver\CustomOperationPathResolver; use ApiPlatform\Core\PathResolver\OperationPathResolver; use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; +use ApiPlatform\Core\Swagger\SchemaFormatter\ChainSchemaFormatter; +use ApiPlatform\Core\Swagger\SchemaFormatter\DefaultDefinitionNormalizer; +use ApiPlatform\Core\Swagger\SchemaFormatter\HydraDefinitionNormalizer; +use ApiPlatform\Core\Swagger\SchemaFormatter\JsonApiDefinitionNormalizer; use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; use ApiPlatform\Core\Tests\Fixtures\DummyFilter; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer; @@ -115,6 +119,9 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), @@ -141,7 +148,8 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -184,7 +192,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -197,7 +205,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -208,7 +216,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'description' => 'Dummy resource created', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'links' => [ @@ -242,7 +250,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -255,7 +263,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The updated Dummy resource', @@ -274,7 +282,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'description' => 'Dummy resource updated', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -311,7 +319,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -325,7 +333,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -335,7 +343,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'description' => 'Dummy resource created', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'links' => [ @@ -354,7 +362,7 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -415,6 +423,10 @@ public function testNormalizeWithNameConverter(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -440,7 +452,8 @@ public function testNormalizeWithNameConverter(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -469,7 +482,7 @@ public function testNormalizeWithNameConverter(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -480,7 +493,7 @@ public function testNormalizeWithNameConverter(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'properties' => [ @@ -549,6 +562,10 @@ public function testNormalizeWithApiKeysEnabled(): void ], ]; + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -574,7 +591,8 @@ public function testNormalizeWithApiKeysEnabled(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -604,7 +622,7 @@ public function testNormalizeWithApiKeysEnabled(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -615,7 +633,7 @@ public function testNormalizeWithApiKeysEnabled(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'properties' => [ @@ -690,6 +708,10 @@ public function testNormalizeWithOnlyNormalizationGroups(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -715,7 +737,8 @@ public function testNormalizeWithOnlyNormalizationGroups(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -749,7 +772,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -763,7 +786,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -773,7 +796,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void 'description' => 'Dummy resource created', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -800,7 +823,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -813,7 +836,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The updated Dummy resource', @@ -832,7 +855,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void 'description' => 'Dummy resource updated', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/'.$ref], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-'.$ref], ], ], ], @@ -844,7 +867,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -855,7 +878,7 @@ public function testNormalizeWithOnlyNormalizationGroups(): void ]), ], ]), - $ref => new \ArrayObject([ + 'application-ld+json-'.$ref => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -904,6 +927,10 @@ public function testNormalizeWithOpenApiDefinitionName(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -929,7 +956,8 @@ public function testNormalizeWithOpenApiDefinitionName(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -958,7 +986,7 @@ public function testNormalizeWithOpenApiDefinitionName(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy-Read'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy-Read'], ], ], ], @@ -969,7 +997,7 @@ public function testNormalizeWithOpenApiDefinitionName(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy-Read' => new \ArrayObject([ + 'application-ld+json-Dummy-Read' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -1026,6 +1054,10 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -1051,7 +1083,8 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -1085,7 +1118,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1098,7 +1131,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -1109,7 +1142,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void 'description' => 'Dummy resource created', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1134,7 +1167,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1147,7 +1180,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy-dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy-dummy'], ], ], 'description' => 'The updated Dummy resource', @@ -1166,7 +1199,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void 'description' => 'Dummy resource updated', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1178,7 +1211,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -1189,7 +1222,7 @@ public function testNormalizeWithOnlyDenormalizationGroups(): void ]), ], ]), - 'Dummy-dummy' => new \ArrayObject([ + 'application-ld+json-Dummy-dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -1247,6 +1280,10 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -1272,7 +1309,8 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -1306,7 +1344,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1319,7 +1357,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -1330,7 +1368,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void 'description' => 'Dummy resource created', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1355,7 +1393,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -1368,7 +1406,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy-dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy-dummy'], ], ], 'description' => 'The updated Dummy resource', @@ -1387,7 +1425,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void 'description' => 'Dummy resource updated', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy-dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy-dummy'], ], ], ], @@ -1399,7 +1437,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -1410,7 +1448,7 @@ public function testNormalizeWithNormalizationAndDenormalizationGroups(): void ]), ], ]), - 'Dummy-dummy' => new \ArrayObject([ + 'application-ld+json-Dummy-dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -1724,17 +1762,20 @@ public function testNormalizeWithNestedNormalizationGroups(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new PropertyNameCollection(['name'])); $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class, ['serializer_groups' => $groups])->shouldBeCalled(1)->willReturn(new PropertyNameCollection(['name'])); + $formats = self::OPERATION_FORMATS; + $formats['output_formats']['json-api'] = ['application/vnd.api+json']; + $dummyMetadata = new ResourceMetadata( 'Dummy', 'This is a dummy.', 'http://schema.example.com/Dummy', [ - 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS, - 'put' => ['method' => 'PUT', 'normalization_context' => [AbstractNormalizer::GROUPS => $groups]] + self::OPERATION_FORMATS, + 'get' => ['method' => 'GET'] + $formats, + 'put' => ['method' => 'PUT', 'normalization_context' => [AbstractNormalizer::GROUPS => $groups]] + $formats, ], [ - 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS, - 'post' => ['method' => 'POST'] + self::OPERATION_FORMATS, + 'get' => ['method' => 'GET'] + $formats, + 'post' => ['method' => 'POST'] + $formats, ] ); @@ -1762,6 +1803,12 @@ public function testNormalizeWithNestedNormalizationGroups(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/ld+json' => new HydraDefinitionNormalizer(), + 'application/vnd.api+json' => new JsonApiDefinitionNormalizer(), + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -1787,7 +1834,8 @@ public function testNormalizeWithNestedNormalizationGroups(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -1821,7 +1869,13 @@ public function testNormalizeWithNestedNormalizationGroups(): void 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], + ], + ], + 'application/vnd.api+json' => [ + 'schema' => [ + 'type' => 'array', + 'items' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-Dummy'], ], ], ], @@ -1834,7 +1888,10 @@ public function testNormalizeWithNestedNormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], + ], + 'application/vnd.api+json' => [ + 'schema' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -1845,7 +1902,10 @@ public function testNormalizeWithNestedNormalizationGroups(): void 'description' => 'Dummy resource created', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], + ], + 'application/vnd.api+json' => [ + 'schema' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-Dummy'], ], ], ], @@ -1870,7 +1930,10 @@ public function testNormalizeWithNestedNormalizationGroups(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], + ], + 'application/vnd.api+json' => [ + 'schema' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-Dummy'], ], ], ], @@ -1883,7 +1946,10 @@ public function testNormalizeWithNestedNormalizationGroups(): void 'requestBody' => [ 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], + ], + 'application/vnd.api+json' => [ + 'schema' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-Dummy'], ], ], 'description' => 'The updated Dummy resource', @@ -1902,7 +1968,10 @@ public function testNormalizeWithNestedNormalizationGroups(): void 'description' => 'Dummy resource updated', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/'.$ref], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-'.$ref], + ], + 'application/vnd.api+json' => [ + 'schema' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-'.$ref], ], ], ], @@ -1914,41 +1983,187 @@ public function testNormalizeWithNestedNormalizationGroups(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], 'properties' => [ - 'name' => new \ArrayObject([ - 'type' => 'string', - 'description' => 'This is a name.', - ]), + 'hydra:member' => [ + 'type' => 'object', + 'properties' => [ + '@type' => [ + 'type' => 'string', + ], + '@id' => [ + 'type' => 'integer', + ], + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ], ], ]), - $ref => new \ArrayObject([ + 'application-ld+json-'.$ref => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], 'properties' => [ - 'name' => new \ArrayObject([ - 'type' => 'string', - 'description' => 'This is a name.', - ]), - 'relatedDummy' => new \ArrayObject([ - 'description' => 'This is a related dummy \o/.', - '$ref' => '#/components/schemas/'.$relatedDummyRef, - ]), + 'hydra:member' => [ + 'type' => 'object', + 'properties' => [ + '@type' => [ + 'type' => 'string', + ], + '@id' => [ + 'type' => 'integer', + ], + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + 'relatedDummy' => new \ArrayObject([ + 'description' => 'This is a related dummy \o/.', + '$ref' => '#/components/schemas/application-ld%2Bjson-'.$relatedDummyRef, + ]), + ], + ], ], ]), - $relatedDummyRef => new \ArrayObject([ + 'application-ld+json-'.$relatedDummyRef => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a related dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/RelatedDummy'], 'properties' => [ - 'name' => new \ArrayObject([ - 'type' => 'string', - 'description' => 'This is a name.', - ]), + 'hydra:member' => [ + 'type' => 'object', + 'properties' => [ + '@id' => [ + 'type' => 'integer', + ], + '@type' => [ + 'type' => 'string', + ], + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ], + ], + ]), + 'application-vnd.api+json-Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'integer', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ], + ], + ], + ], + ]), + 'application-vnd.api+json-'.$ref => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'integer', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ], + 'relationships' => [ + 'type' => 'object', + 'properties' => [ + 'relatedDummy' => [ + 'type' => 'object', + 'properties' => [ + 'links' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'string', + ], + 'related' => [ + 'type' => 'string', + ], + ], + ], + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + ], + ], + ], + ], + ], + ]), + 'application-vnd.api+json-'.$relatedDummyRef => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a related dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/RelatedDummy'], + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => [ + 'type' => 'integer', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ], + ], + ], ], ]), ]), @@ -1983,6 +2198,10 @@ private function normalizeWithFilters($filterLocator): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -2008,7 +2227,8 @@ private function normalizeWithFilters($filterLocator): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -2030,7 +2250,7 @@ private function normalizeWithFilters($filterLocator): void 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -2076,7 +2296,7 @@ private function normalizeWithFilters($filterLocator): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'properties' => [ @@ -2158,6 +2378,15 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator()); + $formatProviderProphecy = $this->prophesize(OperationAwareFormatsProviderInterface::class); + $formatProviderProphecy->getFormatsFromOperation(Question::class, 'get', OperationType::ITEM)->willReturn(['json' => ['application/json'], 'csv' => ['text/csv']]); + $formatProviderProphecy->getFormatsFromOperation(Answer::class, 'get', OperationType::SUBRESOURCE)->willReturn(['xml' => ['text/xml']]); + + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + 'text/xml' => new DefaultDefinitionNormalizer(), //@todo: use correct formatter + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactory, $propertyNameCollectionFactory, @@ -2183,7 +2412,8 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt $formatProvider ?? [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -2212,10 +2442,10 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt 'description' => 'Question resource response', 'content' => [ 'application/json' => [ - 'schema' => ['$ref' => '#/components/schemas/Question'], + 'schema' => ['$ref' => '#/components/schemas/application-json-Question'], ], 'text/csv' => [ - 'schema' => ['$ref' => '#/components/schemas/Question'], + 'schema' => ['$ref' => '#/components/schemas/text-csv-Question'], ], ], ], @@ -2233,7 +2463,7 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt 'description' => 'Answer resource response', 'content' => [ 'text/xml' => [ - 'schema' => ['$ref' => '#/components/schemas/Answer'], + 'schema' => ['$ref' => '#/components/schemas/text-xml-Answer'], ], ], ], @@ -2252,7 +2482,7 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Question' => new \ArrayObject([ + 'application-json-Question' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a question.', 'externalDocs' => ['url' => 'http://schema.example.com/Question'], @@ -2260,11 +2490,11 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt 'answer' => new \ArrayObject([ 'type' => 'array', 'description' => 'This is a name.', - 'items' => ['$ref' => '#/components/schemas/Answer'], + 'items' => ['$ref' => '#/components/schemas/application-json-Answer'], ]), ], ]), - 'Answer' => new \ArrayObject([ + 'application-json-Answer' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is an answer.', 'externalDocs' => ['url' => 'http://schema.example.com/Answer'], @@ -2272,7 +2502,43 @@ private function doTestNormalizeWithSubResource(OperationAwareFormatsProviderInt 'content' => new \ArrayObject([ 'type' => 'array', 'description' => 'This is a name.', - 'items' => ['$ref' => '#/components/schemas/Answer'], + 'items' => ['$ref' => '#/components/schemas/application-json-Answer'], + ]), + ], + ]), + 'text-xml-Answer' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is an answer.', + 'externalDocs' => ['url' => 'http://schema.example.com/Answer'], + 'properties' => [ + 'content' => new \ArrayObject([ + 'type' => 'array', + 'description' => 'This is a name.', + 'items' => ['$ref' => '#/components/schemas/text-xml-Answer'], + ]), + ], + ]), + 'text-csv-Question' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a question.', + 'externalDocs' => ['url' => 'http://schema.example.com/Question'], + 'properties' => [ + 'answer' => new \ArrayObject([ + 'type' => 'array', + 'description' => 'This is a name.', + 'items' => ['$ref' => '#/components/schemas/text-csv-Answer'], + ]), + ], + ]), + 'text-csv-Answer' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is an answer.', + 'externalDocs' => ['url' => 'http://schema.example.com/Answer'], + 'properties' => [ + 'content' => new \ArrayObject([ + 'type' => 'array', + 'description' => 'This is a name.', + 'items' => ['$ref' => '#/components/schemas/text-csv-Answer'], ]), ], ]), @@ -2308,6 +2574,10 @@ public function testNormalizeWithPropertyOpenApiContext(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -2333,7 +2603,8 @@ public function testNormalizeWithPropertyOpenApiContext(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -2363,7 +2634,7 @@ public function testNormalizeWithPropertyOpenApiContext(): void 'description' => 'Dummy resource response', 'content' => [ 'application/ld+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -2374,7 +2645,7 @@ public function testNormalizeWithPropertyOpenApiContext(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -2424,6 +2695,10 @@ public function testNormalizeWithPaginationClientEnabled(): void $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())); + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -2449,7 +2724,8 @@ public function testNormalizeWithPaginationClientEnabled(): void [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -2489,7 +2765,7 @@ public function testNormalizeWithPaginationClientEnabled(): void 'application/ld+json' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-ld%2Bjson-Dummy'], ], ], ], @@ -2500,7 +2776,7 @@ public function testNormalizeWithPaginationClientEnabled(): void ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-ld+json-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], @@ -2572,6 +2848,18 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw $operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator()); + $formatProviderProphecy = $this->prophesize(OperationAwareFormatsProviderInterface::class); + $formatProviderProphecy->getFormatsFromOperation(Dummy::class, 'get', OperationType::COLLECTION)->willReturn(['xml' => ['application/xml', 'text/xml']]); + $formatProviderProphecy->getFormatsFromOperation(Dummy::class, 'post', OperationType::COLLECTION)->willReturn(['xml' => ['text/xml'], 'csv' => ['text/csv']]); + $formatProviderProphecy->getFormatsFromOperation(Dummy::class, 'get', OperationType::ITEM)->willReturn(['jsonapi' => ['application/vnd.api+json']]); + $formatProviderProphecy->getFormatsFromOperation(Dummy::class, 'put', OperationType::ITEM)->willReturn(['json' => ['application/json'], 'csv' => ['text/csv']]); + + $schemaFormatterFactory = new ChainSchemaFormatter([ + 'application/ld+json' => new HydraDefinitionNormalizer(), + 'application/vnd.api+json' => new JsonApiDefinitionNormalizer(), + 'application/json' => new DefaultDefinitionNormalizer(), + ]); + $normalizer = new DocumentationNormalizer( $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), @@ -2596,7 +2884,8 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw $formatsProvider ?? [], false, 'pagination', - ['spec_version' => 3] + ['spec_version' => 3], + $schemaFormatterFactory ); $expected = [ @@ -2620,13 +2909,13 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw 'application/xml' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/application-xml-Dummy'], ], ], 'text/xml' => [ 'schema' => [ 'type' => 'array', - 'items' => ['$ref' => '#/components/schemas/Dummy'], + 'items' => ['$ref' => '#/components/schemas/text-xml-Dummy'], ], ], ], @@ -2640,10 +2929,10 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw 'requestBody' => [ 'content' => [ 'text/xml' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/text-xml-Dummy'], ], 'text/csv' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/text-csv-Dummy'], ], ], 'description' => 'The new Dummy resource', @@ -2653,10 +2942,10 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw 'description' => 'Dummy resource created', 'content' => [ 'text/xml' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/text-xml-Dummy'], ], 'text/csv' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/text-csv-Dummy'], ], ], 'links' => [ @@ -2690,7 +2979,7 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw 'description' => 'Dummy resource response', 'content' => [ 'application/vnd.api+json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-vnd.api%2Bjson-Dummy'], ], ], ], @@ -2712,10 +3001,10 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw 'requestBody' => [ 'content' => [ 'application/json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-json-Dummy'], ], 'text/csv' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/text-csv-Dummy'], ], ], 'description' => 'The updated Dummy resource', @@ -2725,10 +3014,10 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw 'description' => 'Dummy resource updated', 'content' => [ 'application/json' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/application-json-Dummy'], ], 'text/csv' => [ - 'schema' => ['$ref' => '#/components/schemas/Dummy'], + 'schema' => ['$ref' => '#/components/schemas/text-csv-Dummy'], ], ], ], @@ -2740,7 +3029,89 @@ private function doNormalizeWithCustomFormatsDefinedAtOperationLevel(OperationAw ]), 'components' => [ 'schemas' => new \ArrayObject([ - 'Dummy' => new \ArrayObject([ + 'application-json-Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'id' => new \ArrayObject([ + 'type' => 'integer', + 'description' => 'This is an id.', + 'readOnly' => true, + ]), + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ]), + 'application-vnd.api+json-Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + ], + 'id' => new \ArrayObject([ + 'type' => 'integer', + 'readOnly' => true, + 'description' => 'This is an id.', + ]), + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + '_id' => new \ArrayObject([ + 'type' => 'integer', + 'description' => 'This is an id.', + 'readOnly' => true, + ]), + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ], + ], + ], + ], + ]), + 'application-xml-Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'id' => new \ArrayObject([ + 'type' => 'integer', + 'description' => 'This is an id.', + 'readOnly' => true, + ]), + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ]), + 'text-xml-Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'id' => new \ArrayObject([ + 'type' => 'integer', + 'description' => 'This is an id.', + 'readOnly' => true, + ]), + 'name' => new \ArrayObject([ + 'type' => 'string', + 'description' => 'This is a name.', + ]), + ], + ]), + 'text-csv-Dummy' => new \ArrayObject([ 'type' => 'object', 'description' => 'This is a dummy.', 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'],