diff --git a/composer.json b/composer.json
index d5de7dbb567..006dff9d9a1 100644
--- a/composer.json
+++ b/composer.json
@@ -22,7 +22,7 @@
"symfony/http-kernel": "^4.3.7 || ^5.0",
"symfony/property-access": "^3.4 || ^4.0 || ^5.0",
"symfony/property-info": "^3.4 || ^4.0 || ^5.0",
- "symfony/serializer": "^4.3 || ^5.0",
+ "symfony/serializer": "^4.4 || ^5.0",
"symfony/web-link": "^4.1 || ^5.0",
"willdurand/negotiation": "^2.0.3"
},
diff --git a/src/Bridge/Symfony/Bundle/Command/OpenApiCommand.php b/src/Bridge/Symfony/Bundle/Command/OpenApiCommand.php
new file mode 100644
index 00000000000..6bc69f52a86
--- /dev/null
+++ b/src/Bridge/Symfony/Bundle/Command/OpenApiCommand.php
@@ -0,0 +1,93 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Command;
+
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * Dumps Open API documentation.
+ */
+final class OpenApiCommand extends Command
+{
+ private $openApiFactory;
+ private $normalizer;
+
+ public function __construct(OpenApiFactoryInterface $openApiFactory, NormalizerInterface $normalizer)
+ {
+ parent::__construct();
+ $this->openApiFactory = $openApiFactory;
+ $this->normalizer = $normalizer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('api:openapi:export')
+ ->setDescription('Dump the Open API documentation')
+ ->addOption('yaml', 'y', InputOption::VALUE_NONE, 'Dump the documentation in YAML')
+ ->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Write output to file')
+ ->addOption('spec-version', null, InputOption::VALUE_OPTIONAL, 'Open API version to use (2 or 3) (2 is deprecated)', 3)
+ ->addOption('api-gateway', null, InputOption::VALUE_NONE, 'Enable the Amazon API Gateway compatibility mode');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ // Backwards compatibility
+ if (2 === $specVersion = (int) $input->getOption('spec-version')) {
+ $command = $this->getApplication()->find('api:swagger:export');
+
+ return $command->run(new ArrayInput([
+ 'command' => 'api:swagger:export',
+ '--spec-version' => $specVersion,
+ '--yaml' => $input->getOption('yaml'),
+ '--output' => $input->getOption('output'),
+ '--api-gateway' => $input->getOption('api-gateway'),
+ ]), $output);
+ }
+
+ $filesystem = new Filesystem();
+ $io = new SymfonyStyle($input, $output);
+ $data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json');
+ $content = $input->getOption('yaml')
+ ? Yaml::dump($data, 10, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)
+ : (json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ?: '');
+
+ $filename = $input->getOption('output');
+ if ($filename && \is_string($filename)) {
+ $filesystem->dumpFile($filename, $content);
+ $io->success(sprintf('Data written to %s.', $filename));
+
+ return 0;
+ }
+
+ $output->writeln($content);
+
+ return 0;
+ }
+}
diff --git a/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php b/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php
index 9c220501e60..02dfbe45006 100644
--- a/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php
+++ b/src/Bridge/Symfony/Bundle/Command/SwaggerCommand.php
@@ -67,9 +67,8 @@ public function __construct(NormalizerInterface $normalizer, ResourceNameCollect
protected function configure()
{
$this
- ->setName('api:openapi:export')
- ->setAliases(['api:swagger:export'])
- ->setDescription('Dump the OpenAPI documentation')
+ ->setName('api:swagger:export')
+ ->setDescription('Dump the Swagger v2 documentation')
->addOption('yaml', 'y', InputOption::VALUE_NONE, 'Dump the documentation in YAML')
->addOption('spec-version', null, InputOption::VALUE_OPTIONAL, sprintf('OpenAPI version to use (%s)', implode(' or ', $this->swaggerVersions)), $this->swaggerVersions[0] ?? 2)
->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Write output to file')
@@ -90,6 +89,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
throw new InvalidOptionException(sprintf('This tool only supports versions %s of the OpenAPI specification ("%s" given).', implode(', ', $this->swaggerVersions), $version));
}
+ if (3 === (int) $version) {
+ @trigger_error('The command "api:swagger:export" is deprecated for the spec version 3 use "api:openapi:export".', E_USER_DEPRECATED);
+ }
+
$documentation = new Documentation($this->resourceNameCollectionFactory->create(), $this->apiTitle, $this->apiDescription, $this->apiVersion, $this->apiFormats);
$data = $this->normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, ['spec_version' => (int) $version, ApiGatewayNormalizer::API_GATEWAY => $input->getOption('api-gateway')]);
$content = $input->getOption('yaml')
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
index 42d7faf8715..4a33e424ae3 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
@@ -365,6 +365,7 @@ private function registerOAuthConfiguration(ContainerBuilder $container, array $
$container->setParameter('api_platform.oauth.flow', $config['oauth']['flow']);
$container->setParameter('api_platform.oauth.tokenUrl', $config['oauth']['tokenUrl']);
$container->setParameter('api_platform.oauth.authorizationUrl', $config['oauth']['authorizationUrl']);
+ $container->setParameter('api_platform.oauth.refreshUrl', $config['oauth']['refreshUrl']);
$container->setParameter('api_platform.oauth.scopes', $config['oauth']['scopes']);
}
@@ -381,6 +382,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array
$loader->load('json_schema.xml');
$loader->load('swagger.xml');
+ $loader->load('openapi.xml');
$loader->load('swagger-ui.xml');
if (!$config['enable_swagger_ui'] && !$config['enable_re_doc']) {
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
index 536d15ad886..51ca4471e2b 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
@@ -249,6 +249,7 @@ private function addOAuthSection(ArrayNodeDefinition $rootNode): void
->scalarNode('flow')->defaultValue('application')->info('The oauth flow grant type.')->end()
->scalarNode('tokenUrl')->defaultValue('/oauth/v2/token')->info('The oauth token url.')->end()
->scalarNode('authorizationUrl')->defaultValue('/oauth/v2/auth')->info('The oauth authentication url.')->end()
+ ->scalarNode('refreshUrl')->defaultValue('/oauth/v2/refresh')->info('The oauth refresh url.')->end()
->arrayNode('scopes')
->prototype('scalar')->end()
->end()
@@ -296,7 +297,7 @@ private function addSwaggerSection(ArrayNodeDefinition $rootNode): void
->addDefaultsIfNotSet()
->children()
->arrayNode('versions')
- ->info('The active versions of OpenAPI to be exported or used in the swagger_ui. The first value is the default.')
+ ->info('The active versions of Open API to be exported or used in the swagger_ui. The first value is the default.')
->defaultValue($defaultVersions)
->beforeNormalization()
->always(static function ($v) {
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml b/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml
index 471da830649..a838e119689 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/data_provider.xml
@@ -33,6 +33,17 @@
%api_platform.graphql.collection.pagination%
+
+
+
+
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml
new file mode 100644
index 00000000000..03bda2f24da
--- /dev/null
+++ b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ %api_platform.title%
+ %api_platform.description%
+ %api_platform.version%
+ %api_platform.oauth.enabled%
+ %api_platform.oauth.type%
+ %api_platform.oauth.flow%
+ %api_platform.oauth.tokenUrl%
+ %api_platform.oauth.authorizationUrl%
+ %api_platform.oauth.refreshUrl%
+ %api_platform.oauth.scopes%
+ %api_platform.swagger.api_keys%
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %api_platform.formats%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DataProvider/PaginationOptions.php b/src/DataProvider/PaginationOptions.php
new file mode 100644
index 00000000000..1db7646943f
--- /dev/null
+++ b/src/DataProvider/PaginationOptions.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\DataProvider;
+
+final class PaginationOptions
+{
+ private $paginationEnabled;
+ private $paginationPageParameterName;
+ private $clientItemsPerPage;
+ private $itemsPerPageParameterName;
+ private $paginationClientEnabled;
+ private $paginationClientEnabledParameterName;
+
+ public function __construct(bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination')
+ {
+ $this->paginationEnabled = $paginationEnabled;
+ $this->paginationPageParameterName = $paginationPageParameterName;
+ $this->clientItemsPerPage = $clientItemsPerPage;
+ $this->itemsPerPageParameterName = $itemsPerPageParameterName;
+ $this->paginationClientEnabled = $paginationClientEnabled;
+ $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
+ }
+
+ public function isPaginationEnabled(): bool
+ {
+ return $this->paginationEnabled;
+ }
+
+ public function getPaginationPageParameterName(): string
+ {
+ return $this->paginationPageParameterName;
+ }
+
+ public function getClientItemsPerPage(): bool
+ {
+ return $this->clientItemsPerPage;
+ }
+
+ public function getItemsPerPageParameterName(): string
+ {
+ return $this->itemsPerPageParameterName;
+ }
+
+ public function getPaginationClientEnabled(): bool
+ {
+ return $this->paginationClientEnabled;
+ }
+
+ public function getPaginationClientEnabledParameterName(): string
+ {
+ return $this->paginationClientEnabledParameterName;
+ }
+}
diff --git a/src/Documentation/Action/DocumentationAction.php b/src/Documentation/Action/DocumentationAction.php
index 57fd1698372..754fc013b4f 100644
--- a/src/Documentation/Action/DocumentationAction.php
+++ b/src/Documentation/Action/DocumentationAction.php
@@ -15,7 +15,9 @@
use ApiPlatform\Core\Api\FormatsProviderInterface;
use ApiPlatform\Core\Documentation\Documentation;
+use ApiPlatform\Core\Documentation\DocumentationInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Request;
@@ -33,18 +35,24 @@ final class DocumentationAction
private $formats;
private $formatsProvider;
private $swaggerVersions;
+ private $openApiFactory;
/**
* @param int[] $swaggerVersions
* @param mixed|array|FormatsProviderInterface $formatsProvider
*/
- public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, string $title = '', string $description = '', string $version = '', $formatsProvider = null, array $swaggerVersions = [2, 3])
+ public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, string $title = '', string $description = '', string $version = '', $formatsProvider = null, array $swaggerVersions = [2, 3], OpenApiFactoryInterface $openApiFactory = null)
{
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->title = $title;
$this->description = $description;
$this->version = $version;
$this->swaggerVersions = $swaggerVersions;
+ $this->openApiFactory = $openApiFactory;
+
+ if (null === $openApiFactory) {
+ @trigger_error(sprintf('Not passing an instance of "%s" as 7th parameter of the constructor of "%s" is deprecated since API Platform 2.6', OpenApiFactoryInterface::class, __CLASS__), E_USER_DEPRECATED);
+ }
if (null === $formatsProvider) {
return;
@@ -56,13 +64,14 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
return;
}
+
$this->formatsProvider = $formatsProvider;
}
- public function __invoke(Request $request = null): Documentation
+ public function __invoke(Request $request = null): DocumentationInterface
{
if (null !== $request) {
- $context = ['base_url' => $request->getBaseUrl(), 'spec_version' => $request->query->getInt('spec_version', $this->swaggerVersions[0] ?? 2)];
+ $context = ['base_url' => $request->getBaseUrl(), 'spec_version' => $request->query->getInt('spec_version', $this->swaggerVersions[0] ?? 3)];
if ($request->query->getBoolean('api_gateway')) {
$context['api_gateway'] = true;
}
@@ -70,11 +79,16 @@ public function __invoke(Request $request = null): Documentation
$attributes = RequestAttributesExtractor::extractAttributes($request);
}
+
// BC check to be removed in 3.0
if (null !== $this->formatsProvider) {
$this->formats = $this->formatsProvider->getFormatsFromAttributes($attributes ?? []);
}
+ if (null !== $this->openApiFactory && isset($context) && 3 === $context['spec_version']) {
+ return $this->openApiFactory->__invoke($context ?? []);
+ }
+
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version, $this->formats);
}
}
diff --git a/src/Documentation/Documentation.php b/src/Documentation/Documentation.php
index 53f5372df28..67228a77c5e 100644
--- a/src/Documentation/Documentation.php
+++ b/src/Documentation/Documentation.php
@@ -20,7 +20,7 @@
*
* @author Amrouche Hamza
*/
-final class Documentation
+final class Documentation implements DocumentationInterface
{
private $resourceNameCollection;
private $title;
diff --git a/src/Documentation/DocumentationInterface.php b/src/Documentation/DocumentationInterface.php
new file mode 100644
index 00000000000..0bbea27c30c
--- /dev/null
+++ b/src/Documentation/DocumentationInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * 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\Documentation;
+
+/**
+ * An API documentation.
+ */
+interface DocumentationInterface
+{
+}
diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php
new file mode 100644
index 00000000000..7ce8cc4d137
--- /dev/null
+++ b/src/OpenApi/Factory/OpenApiFactory.php
@@ -0,0 +1,448 @@
+
+ *
+ * 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\OpenApi\Factory;
+
+use ApiPlatform\Core\Api\FilterLocatorTrait;
+use ApiPlatform\Core\Api\OperationType;
+use ApiPlatform\Core\DataProvider\PaginationOptions;
+use ApiPlatform\Core\JsonSchema\Schema;
+use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
+use ApiPlatform\Core\JsonSchema\TypeFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\OpenApi\Model;
+use ApiPlatform\Core\OpenApi\Model\ExternalDocumentation;
+use ApiPlatform\Core\OpenApi\OpenApi;
+use ApiPlatform\Core\OpenApi\Options;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
+use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\PropertyInfo\Type;
+
+/**
+ * Generates an Open API v3 specification.
+ */
+final class OpenApiFactory implements OpenApiFactoryInterface
+{
+ use FilterLocatorTrait;
+
+ public const BASE_URL = 'base_url';
+
+ private $resourceNameCollectionFactory;
+ private $resourceMetadataFactory;
+ private $propertyNameCollectionFactory;
+ private $propertyMetadataFactory;
+ private $operationPathResolver;
+ private $subresourceOperationFactory;
+ private $formats;
+ private $jsonSchemaFactory;
+ private $jsonSchemaTypeFactory;
+ private $openApiOptions;
+ private $paginationOptions;
+
+ public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, SchemaFactoryInterface $jsonSchemaFactory, TypeFactoryInterface $jsonSchemaTypeFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, SubresourceOperationFactoryInterface $subresourceOperationFactory, array $formats = [], Options $openApiOptions, PaginationOptions $paginationOptions)
+ {
+ $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
+ $this->jsonSchemaFactory = $jsonSchemaFactory;
+ $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory;
+ $this->formats = $formats;
+ $this->setFilterLocator($filterLocator, true);
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
+ $this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->operationPathResolver = $operationPathResolver;
+ $this->openApiOptions = $openApiOptions;
+ $this->paginationOptions = $paginationOptions;
+ $this->subresourceOperationFactory = $subresourceOperationFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __invoke(array $context = []): OpenApi
+ {
+ $baseUrl = $context[self::BASE_URL] ?? '/';
+ $info = new Model\Info($this->openApiOptions->getTitle(), $this->openApiOptions->getVersion(), trim($this->openApiOptions->getDescription()));
+ $servers = '/' === $baseUrl || '' === $baseUrl ? [] : [new Model\Server($baseUrl)];
+ $paths = new Model\Paths();
+ $links = [];
+ $schemas = [];
+
+ foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
+ $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
+ $resourceShortName = $resourceMetadata->getShortName();
+
+ // Items needs to be parsed first to be able to reference the lines from the collection operation
+ list($itemOperationLinks, $itemOperationSchemas) = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::ITEM, $context, $paths, $links, $schemas);
+ $schemas += $itemOperationSchemas;
+ list($collectionOperationLinks, $collectionOperationSchemas) = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::COLLECTION, $context, $paths, $links, $schemas);
+
+ list($subresourceOperationLinks, $subresourceOperationSchemas) = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::SUBRESOURCE, $context, $paths, $links, $schemas);
+ $schemas += $collectionOperationSchemas;
+ }
+
+ $securitySchemes = $this->getSecuritySchemes();
+ $securityRequirements = [];
+
+ foreach (array_keys($securitySchemes) as $key) {
+ $securityRequirements[$key] = [];
+ }
+
+ return new OpenApi($info, $servers, $paths, new Model\Components(new \ArrayObject($schemas), new \ArrayObject(), new \ArrayObject(), new \ArrayObject(), new \ArrayObject(), new \ArrayObject(), new \ArrayObject($securitySchemes)), $securityRequirements);
+ }
+
+ /**
+ * @return array | array
+ */
+ private function collectPaths(ResourceMetadata $resourceMetadata, string $resourceClass, string $operationType, array $context, Model\Paths $paths, array &$links, array $schemas = []): array
+ {
+ $resourceShortName = $resourceMetadata->getShortName();
+ $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : (OperationType::ITEM === $operationType ? $resourceMetadata->getItemOperations() : $this->subresourceOperationFactory->create($resourceClass));
+ if (!$operations) {
+ return [$links, $schemas];
+ }
+
+ foreach ($operations as $operationName => $operation) {
+ $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
+ $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET');
+ list($requestMimeTypes, $responseMimeTypes) = $this->getMimeTypes($resourceClass, $operationName, $operationType, $resourceMetadata);
+ $operationId = $operation['openapi_context']['operationId'] ?? lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
+ $linkedOperationId = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM);
+ $pathItem = $paths->getPath($path) ?: new Model\PathItem();
+
+ $operationOutputSchemas = [];
+ foreach ($responseMimeTypes as $operationFormat) {
+ $operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operationType, $operationName, new Schema('openapi'), $context);
+ $schemas += $operationOutputSchema->getDefinitions()->getArrayCopy();
+ $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
+ }
+
+ $parameters = [];
+ $responses = [];
+
+ // Set up parameters
+ if (OperationType::ITEM === $operationType) {
+ $parameters[] = new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string']);
+ $links[$operationId] = $this->getLink($resourceClass, $operationId, $path);
+ } elseif (OperationType::COLLECTION === $operationType && 'GET' === $method) {
+ $parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass));
+ } elseif (OperationType::SUBRESOURCE === $operationType) {
+ // FIXME: In SubresourceOperationFactory identifiers may happen twice
+ $added = [];
+ foreach ($operation['identifiers'] as $identifier) {
+ if (\in_array($identifier[0], $added, true)) {
+ continue;
+ }
+ $added[] = $identifier[0];
+ $parameterShortname = $this->resourceMetadataFactory->create($identifier[1])->getShortName();
+ $parameters[] = new Model\Parameter($identifier[0], 'path', $parameterShortname.' identifier', true, false, false, ['type' => 'string']);
+ }
+
+ if ($operation['collection']) {
+ $parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass));
+ }
+ }
+
+ // Create responses
+ switch ($method) {
+ case 'GET':
+ $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');
+ $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas);
+ $responses[$successStatus] = new Model\Response(sprintf('%s %s', $resourceShortName, OperationType::COLLECTION === $operationType ? 'collection' : 'resource'), $responseContent);
+ break;
+ case 'POST':
+ $responseLinks = new \ArrayObject(isset($links[$linkedOperationId]) ? [ucfirst($linkedOperationId) => $links[$linkedOperationId]] : []);
+ $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas);
+ $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '201');
+ $responses[$successStatus] = new Model\Response(sprintf('%s resource created', $resourceShortName), $responseContent, null, $responseLinks);
+ $responses['400'] = new Model\Response('Invalid input');
+ break;
+ case 'PATCH':
+ case 'PUT':
+ $responseLinks = new \ArrayObject(isset($links[$linkedOperationId]) ? [ucfirst($linkedOperationId) => $links[$linkedOperationId]] : []);
+ $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');
+ $responseContent = $this->buildContent($responseMimeTypes, $operationOutputSchemas);
+ $responses[$successStatus] = new Model\Response(sprintf('%s resource updated', $resourceShortName), $responseContent, null, $responseLinks);
+ $responses['400'] = new Model\Response('Invalid input');
+ break;
+ case 'DELETE':
+ $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '204');
+ $responses[$successStatus] = new Model\Response(sprintf('%s resource deleted', $resourceShortName));
+ break;
+ }
+
+ if (OperationType::ITEM === $operationType) {
+ $responses['404'] = new Model\Response('Resource not found');
+ }
+
+ if (!$responses) {
+ $responses['default'] = new Model\Response('Unexpected error');
+ }
+
+ $requestBody = null;
+ if ('PUT' === $method || 'POST' === $method || 'PATCH' === $method) {
+ $operationInputSchemas = [];
+ foreach ($requestMimeTypes as $operationFormat) {
+ $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operationType, $operationName, new Schema('openapi'), $context);
+ $schemas += $operationInputSchema->getDefinitions()->getArrayCopy();
+ $operationInputSchemas[$operationFormat] = $operationInputSchema;
+ }
+
+ $requestBody = new Model\RequestBody(sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName), $this->buildContent($requestMimeTypes, $operationInputSchemas), true);
+ }
+
+ $pathItem = $pathItem->{'with'.ucfirst($method)}(new Model\Operation(
+ $operationId,
+ $operation['openapi_context']['tags'] ?? (OperationType::SUBRESOURCE === $operationType ? $operation['shortNames'] : [$resourceShortName]),
+ $responses,
+ $operation['openapi_context']['summary'] ?? '',
+ $operation['openapi_context']['description'] ?? $this->getPathDescription($resourceShortName, $method, $operationType),
+ isset($operation['openapi_context']['externalDocs']) ? new ExternalDocumentation($operation['openapi_context']['externalDocs']['description'] ?? null, $operation['openapi_context']['externalDocs']['url']) : null,
+ $parameters,
+ $requestBody,
+ isset($operation['openapi_context']['callbacks']) ? new \ArrayObject($operation['openapi_context']['callbacks']) : null,
+ $operation['openapi_context']['deprecated'] ?? (bool) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', false, true),
+ $operation['openapi_context']['security'] ?? [],
+ $operation['openapi_context']['servers'] ?? []
+ ));
+
+ $paths->addPath($path, $pathItem);
+ }
+
+ return [$links, $schemas];
+ }
+
+ private function buildContent(array $responseMimeTypes, array $operationSchemas): \ArrayObject
+ {
+ $content = new \ArrayObject();
+
+ foreach ($responseMimeTypes as $mimeType => $format) {
+ $content[$mimeType] = new Model\MediaType(new \ArrayObject($operationSchemas[$format]->getArrayCopy(false)));
+ }
+
+ return $content;
+ }
+
+ private function getMimeTypes(string $resourceClass, string $operationName, string $operationType, ResourceMetadata $resourceMetadata = null): array
+ {
+ $requestFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_formats', $this->formats, true);
+ $responseFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_formats', $this->formats, true);
+
+ $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
+ $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
+
+ return [$requestMimeTypes, $responseMimeTypes];
+ }
+
+ private function flattenMimeTypes(array $responseFormats): array
+ {
+ $responseMimeTypes = [];
+ foreach ($responseFormats as $responseFormat => $mimeTypes) {
+ foreach ($mimeTypes as $mimeType) {
+ $responseMimeTypes[$mimeType] = $responseFormat;
+ }
+ }
+
+ return $responseMimeTypes;
+ }
+
+ /**
+ * Gets the path for an operation.
+ *
+ * If the path ends with the optional _format parameter, it is removed
+ * as optional path parameters are not yet supported.
+ *
+ * @see https://github.com/OAI/OpenAPI-Specification/issues/93
+ */
+ private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
+ {
+ if ($operation['path'] ?? null) {
+ return 0 === strpos($operation['path'], '/') ? $operation['path'] : '/'.$operation['path'];
+ }
+ $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
+ if ('.{_format}' === substr($path, -10)) {
+ $path = substr($path, 0, -10);
+ }
+
+ return $path;
+ }
+
+ private function getPathDescription(string $resourceShortName, string $method, string $operationType): string
+ {
+ switch ($method) {
+ case 'GET':
+ $pathSummary = OperationType::COLLECTION === $operationType ? 'Retrieves the collection of %s resources.' : 'Retrieves a %s resource.';
+ break;
+ case 'POST':
+ $pathSummary = 'Creates a %s resource.';
+ break;
+ case 'PATCH':
+ $pathSummary = 'Updates the %s resource.';
+ break;
+ case 'PUT':
+ $pathSummary = 'Replaces the %s resource.';
+ break;
+ case 'DELETE':
+ $pathSummary = 'Removes the %s resource.';
+ break;
+ default:
+ return $resourceShortName;
+ }
+
+ return sprintf($pathSummary, $resourceShortName);
+ }
+
+ /**
+ * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
+ */
+ private function getLink(string $resourceClass, string $operationId, string $path): Model\Link
+ {
+ $parameters = [];
+
+ foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
+ $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
+ if (!$propertyMetadata->isIdentifier()) {
+ continue;
+ }
+
+ $parameters[$propertyName] = sprintf('$response.body#/%s', $propertyName);
+ }
+
+ return new Model\Link(
+ $operationId,
+ new \ArrayObject($parameters),
+ [],
+ 1 === \count($parameters) ? sprintf('The `%1$s` value returned in the response can be used as the `%1$s` parameter in `GET %2$s`.', key($parameters), $path) : sprintf('The values returned in the response can be used in `GET %s`.', $path)
+ );
+ }
+
+ /**
+ * Gets parameters corresponding to enabled filters.
+ */
+ private function getFiltersParameters(ResourceMetadata $resourceMetadata, string $operationName, string $resourceClass): array
+ {
+ $parameters = [];
+ $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
+ foreach ($resourceFilters as $filterId) {
+ if (!$filter = $this->getFilter($filterId)) {
+ continue;
+ }
+
+ foreach ($filter->getDescription($resourceClass) as $name => $data) {
+ $schema = $data['schema'] ?? \in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string'];
+
+ $parameters[] = new Model\Parameter(
+ $name,
+ 'query',
+ $data['description'] ?? '',
+ $data['required'] ?? false,
+ $data['openapi']['deprecated'] ?? false,
+ $data['openapi']['allowEmptyValue'] ?? true,
+ $schema,
+ 'array' === $schema['type'] && \in_array($data['type'],
+ [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true) ? 'deepObject' : 'form',
+ 'array' === $schema['type'],
+ $data['openapi']['allowReserved'] ?? false,
+ $data['openapi']['example'] ?? null,
+ isset($data['openapi']['examples']
+ ) ? new \ArrayObject($data['openapi']['examples']) : null);
+ }
+ }
+
+ return $parameters;
+ }
+
+ private function getPaginationParameters(ResourceMetadata $resourceMetadata, string $operationName): array
+ {
+ if (!$this->paginationOptions->isPaginationEnabled()) {
+ return [];
+ }
+
+ $parameters = [];
+
+ if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
+ $parameters[] = new Model\Parameter($this->paginationOptions->getPaginationPageParameterName(), 'query', 'The collection page number', false, false, true, ['type' => 'integer', 'default' => 1]);
+
+ if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->paginationOptions->getClientItemsPerPage(), true)) {
+ $schema = [
+ 'type' => 'integer',
+ 'default' => $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', 30, true),
+ 'minimum' => 0,
+ ];
+
+ if (null !== $maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_maximum_items_per_page', null, true)) {
+ $schema['maximum'] = $maxItemsPerPage;
+ }
+
+ $parameters[] = new Model\Parameter($this->paginationOptions->getItemsPerPageParameterName(), 'query', 'The number of items per page', false, false, true, $schema);
+ }
+ }
+
+ if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationOptions->getPaginationClientEnabled(), true)) {
+ $parameters[] = new Model\Parameter($this->paginationOptions->getPaginationClientEnabledParameterName(), 'query', 'Enable or disable pagination', false, false, true, ['type' => 'boolean']);
+ }
+
+ return $parameters;
+ }
+
+ private function getOauthSecurityScheme(): Model\SecurityScheme
+ {
+ $oauthFlow = new Model\OAuthFlow($this->openApiOptions->getOAuthAuthorizationUrl(), $this->openApiOptions->getOAuthTokenUrl(), $this->openApiOptions->getOAuthRefreshUrl(), new \ArrayObject($this->openApiOptions->getOAuthScopes()));
+ $description = sprintf(
+ 'OAuth 2.0 %s Grant',
+ strtolower(preg_replace('/[A-Z]/', ' \\0', lcfirst($this->openApiOptions->getOAuthFlow())))
+ );
+ $implicit = $password = $clientCredentials = $authorizationCode = null;
+
+ switch ($this->openApiOptions->getOAuthFlow()) {
+ case 'implicit':
+ $implicit = $oauthFlow;
+ break;
+ case 'password':
+ $password = $oauthFlow;
+ break;
+ case 'application':
+ case 'clientCredentials':
+ $clientCredentials = $oauthFlow;
+ break;
+ case 'accessCode':
+ case 'authorizationCode':
+ $authorizationCode = $oauthFlow;
+ break;
+ default:
+ throw new \LogicException('OAuth flow must be one of: implicit, password, clientCredentials, authorizationCode');
+ }
+
+ return new Model\SecurityScheme($this->openApiOptions->getOAuthType(), $description, null, null, 'oauth2', null, new Model\OAuthFlows($implicit, $password, $clientCredentials, $authorizationCode), null);
+ }
+
+ private function getSecuritySchemes(): array
+ {
+ $securitySchemes = [];
+
+ if ($this->openApiOptions->getOAuthEnabled()) {
+ $securitySchemes['oauth'] = $this->getOauthSecurityScheme();
+ }
+
+ foreach ($this->openApiOptions->getApiKeys() as $key => $apiKey) {
+ $description = sprintf('Value for the %s %s parameter.', $apiKey['name'], $apiKey['type']);
+ $securitySchemes[$key] = new Model\SecurityScheme('apiKey', $description, $apiKey['name'], $apiKey['type'], 'bearer');
+ }
+
+ return $securitySchemes;
+ }
+}
diff --git a/src/OpenApi/Factory/OpenApiFactoryInterface.php b/src/OpenApi/Factory/OpenApiFactoryInterface.php
new file mode 100644
index 00000000000..d4b04bb190f
--- /dev/null
+++ b/src/OpenApi/Factory/OpenApiFactoryInterface.php
@@ -0,0 +1,24 @@
+
+ *
+ * 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\OpenApi\Factory;
+
+use ApiPlatform\Core\OpenApi\OpenApi;
+
+interface OpenApiFactoryInterface
+{
+ /**
+ * Creates an OpenApi class.
+ */
+ public function __invoke(array $context = []): OpenApi;
+}
diff --git a/src/OpenApi/Model/Components.php b/src/OpenApi/Model/Components.php
new file mode 100644
index 00000000000..1035ab1d7f0
--- /dev/null
+++ b/src/OpenApi/Model/Components.php
@@ -0,0 +1,159 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Components
+{
+ use ExtensionTrait;
+
+ private $schemas;
+ private $responses;
+ private $parameters;
+ private $examples;
+ private $requestBodies;
+ private $headers;
+ private $securitySchemes;
+ private $links;
+ private $callbacks;
+
+ public function __construct(\ArrayObject $schemas = null, \ArrayObject $responses = null, \ArrayObject $parameters = null, \ArrayObject $examples = null, \ArrayObject $requestBodies = null, \ArrayObject $headers = null, \ArrayObject $securitySchemes = null, \ArrayObject $links = null, \ArrayObject $callbacks = null)
+ {
+ $this->schemas = $schemas;
+ $this->responses = $responses;
+ $this->parameters = $parameters;
+ $this->examples = $examples;
+ $this->requestBodies = $requestBodies;
+ $this->headers = $headers;
+ $this->securitySchemes = $securitySchemes;
+ $this->links = $links;
+ $this->callbacks = $callbacks;
+ }
+
+ public function getSchemas(): ?\ArrayObject
+ {
+ return $this->schemas;
+ }
+
+ public function getResponses(): ?\ArrayObject
+ {
+ return $this->responses;
+ }
+
+ public function getParameters(): ?\ArrayObject
+ {
+ return $this->parameters;
+ }
+
+ public function getExamples(): ?\ArrayObject
+ {
+ return $this->examples;
+ }
+
+ public function getRequestBodies(): ?\ArrayObject
+ {
+ return $this->requestBodies;
+ }
+
+ public function getHeaders(): ?\ArrayObject
+ {
+ return $this->headers;
+ }
+
+ public function getSecuritySchemes(): ?\ArrayObject
+ {
+ return $this->securitySchemes;
+ }
+
+ public function getLinks(): ?\ArrayObject
+ {
+ return $this->links;
+ }
+
+ public function getCallbacks(): ?\ArrayObject
+ {
+ return $this->callbacks;
+ }
+
+ public function withSchemas(\ArrayObject $schemas): self
+ {
+ $clone = clone $this;
+ $clone->schemas = $schemas;
+
+ return $clone;
+ }
+
+ public function withResponses(\ArrayObject $responses): self
+ {
+ $clone = clone $this;
+ $clone->responses = $responses;
+
+ return $clone;
+ }
+
+ public function withParameters(\ArrayObject $parameters): self
+ {
+ $clone = clone $this;
+ $clone->parameters = $parameters;
+
+ return $clone;
+ }
+
+ public function withExamples(\ArrayObject $examples): self
+ {
+ $clone = clone $this;
+ $clone->examples = $examples;
+
+ return $clone;
+ }
+
+ public function withRequestBodies(\ArrayObject $requestBodies): self
+ {
+ $clone = clone $this;
+ $clone->requestBodies = $requestBodies;
+
+ return $clone;
+ }
+
+ public function withHeaders(\ArrayObject $headers): self
+ {
+ $clone = clone $this;
+ $clone->headers = $headers;
+
+ return $clone;
+ }
+
+ public function withSecuritySchemes(\ArrayObject $securitySchemes): self
+ {
+ $clone = clone $this;
+ $clone->securitySchemes = $securitySchemes;
+
+ return $clone;
+ }
+
+ public function withLinks(\ArrayObject $links): self
+ {
+ $clone = clone $this;
+ $clone->links = $links;
+
+ return $clone;
+ }
+
+ public function withCallbacks(\ArrayObject $callbacks): self
+ {
+ $clone = clone $this;
+ $clone->callbacks = $callbacks;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Contact.php b/src/OpenApi/Model/Contact.php
new file mode 100644
index 00000000000..713de0ff65e
--- /dev/null
+++ b/src/OpenApi/Model/Contact.php
@@ -0,0 +1,69 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Contact
+{
+ use ExtensionTrait;
+
+ private $name;
+ private $url;
+ private $email;
+
+ public function __construct(string $name = '', string $url = '', string $email = '')
+ {
+ $this->name = $name;
+ $this->url = $url;
+ $this->email = $email;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ public function getEmail(): string
+ {
+ return $this->email;
+ }
+
+ public function withName(string $name): self
+ {
+ $clone = clone $this;
+ $clone->name = $name;
+
+ return $clone;
+ }
+
+ public function withUrl(string $url): self
+ {
+ $clone = clone $this;
+ $clone->url = $url;
+
+ return $clone;
+ }
+
+ public function withEmail(string $email): self
+ {
+ $clone = clone $this;
+ $clone->email = $email;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Encoding.php b/src/OpenApi/Model/Encoding.php
new file mode 100644
index 00000000000..12b8d91a1d2
--- /dev/null
+++ b/src/OpenApi/Model/Encoding.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\OpenApi\Model;
+
+final class Encoding
+{
+ use ExtensionTrait;
+
+ private $contentType;
+ private $headers;
+ private $style;
+ private $explode;
+ private $allowReserved;
+
+ public function __construct(string $contentType = '', \ArrayObject $headers = null, string $style = '', bool $explode = false, bool $allowReserved = false)
+ {
+ $this->contentType = $contentType;
+ $this->headers = $headers;
+ $this->style = $style;
+ $this->explode = $explode;
+ $this->allowReserved = $allowReserved;
+ }
+
+ public function getContentType(): string
+ {
+ return $this->contentType;
+ }
+
+ public function getHeaders(): ?\ArrayObject
+ {
+ return $this->headers;
+ }
+
+ public function getStyle(): string
+ {
+ return $this->style;
+ }
+
+ public function canExplode(): bool
+ {
+ return $this->explode;
+ }
+
+ public function canAllowReserved(): bool
+ {
+ return $this->allowReserved;
+ }
+
+ public function withContentType(string $contentType): self
+ {
+ $clone = clone $this;
+ $clone->contentType = $contentType;
+
+ return $clone;
+ }
+
+ public function withHeaders(?\ArrayObject $headers): self
+ {
+ $clone = clone $this;
+ $clone->headers = $headers;
+
+ return $clone;
+ }
+
+ public function withStyle(string $style): self
+ {
+ $clone = clone $this;
+ $clone->style = $style;
+
+ return $clone;
+ }
+
+ public function withExplode(bool $explode): self
+ {
+ $clone = clone $this;
+ $clone->explode = $explode;
+
+ return $clone;
+ }
+
+ public function withAllowReserved(bool $allowReserved): self
+ {
+ $clone = clone $this;
+ $clone->allowReserved = $allowReserved;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/ExtensionTrait.php b/src/OpenApi/Model/ExtensionTrait.php
new file mode 100644
index 00000000000..62f97f2151d
--- /dev/null
+++ b/src/OpenApi/Model/ExtensionTrait.php
@@ -0,0 +1,36 @@
+
+ *
+ * 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\OpenApi\Model;
+
+trait ExtensionTrait
+{
+ private $extensionProperties = [];
+
+ public function withExtensionProperty(string $key, string $value)
+ {
+ if (0 !== strpos($key, 'x-')) {
+ $key = 'x-'.$key;
+ }
+
+ $clone = clone $this;
+ $clone->extensionProperties[$key] = $value;
+
+ return $clone;
+ }
+
+ public function getExtensionProperties(): array
+ {
+ return $this->extensionProperties;
+ }
+}
diff --git a/src/OpenApi/Model/ExternalDocumentation.php b/src/OpenApi/Model/ExternalDocumentation.php
new file mode 100644
index 00000000000..f606a26b4d7
--- /dev/null
+++ b/src/OpenApi/Model/ExternalDocumentation.php
@@ -0,0 +1,54 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class ExternalDocumentation
+{
+ use ExtensionTrait;
+
+ private $description;
+ private $url;
+
+ public function __construct(string $description = '', string $url = '')
+ {
+ $this->description = $description;
+ $this->url = $url;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withUrl(string $url): self
+ {
+ $clone = clone $this;
+ $clone->url = $url;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Info.php b/src/OpenApi/Model/Info.php
new file mode 100644
index 00000000000..9b16e9e8909
--- /dev/null
+++ b/src/OpenApi/Model/Info.php
@@ -0,0 +1,114 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Info
+{
+ use ExtensionTrait;
+
+ private $title;
+ private $description;
+ private $termsOfService;
+ private $contact;
+ private $license;
+ private $version;
+
+ public function __construct(string $title, string $version, string $description = '', string $termsOfService = null, Contact $contact = null, License $license = null)
+ {
+ $this->title = $title;
+ $this->version = $version;
+ $this->description = $description;
+ $this->termsOfService = $termsOfService;
+ $this->contact = $contact;
+ $this->license = $license;
+ }
+
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getTermsOfService(): ?string
+ {
+ return $this->termsOfService;
+ }
+
+ public function getContact(): ?Contact
+ {
+ return $this->contact;
+ }
+
+ public function getLicense(): ?License
+ {
+ return $this->license;
+ }
+
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ public function withTitle(string $title): self
+ {
+ $info = clone $this;
+ $info->title = $title;
+
+ return $info;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withTermsOfService(string $termsOfService): self
+ {
+ $clone = clone $this;
+ $clone->termsOfService = $termsOfService;
+
+ return $clone;
+ }
+
+ public function withContact(Contact $contact): self
+ {
+ $clone = clone $this;
+ $clone->contact = $contact;
+
+ return $clone;
+ }
+
+ public function withLicense(License $license): self
+ {
+ $clone = clone $this;
+ $clone->license = $license;
+
+ return $clone;
+ }
+
+ public function withVersion(string $version): self
+ {
+ $clone = clone $this;
+ $clone->version = $version;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/License.php b/src/OpenApi/Model/License.php
new file mode 100644
index 00000000000..ebc183c9aa0
--- /dev/null
+++ b/src/OpenApi/Model/License.php
@@ -0,0 +1,54 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class License
+{
+ use ExtensionTrait;
+
+ private $name;
+ private $url;
+
+ public function __construct(string $name, string $url)
+ {
+ $this->name = $name;
+ $this->url = $url;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ public function withName(string $name): self
+ {
+ $clone = clone $this;
+ $clone->name = $name;
+
+ return $clone;
+ }
+
+ public function withUrl(string $url): self
+ {
+ $clone = clone $this;
+ $clone->url = $url;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Link.php b/src/OpenApi/Model/Link.php
new file mode 100644
index 00000000000..a0feb9511d6
--- /dev/null
+++ b/src/OpenApi/Model/Link.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\OpenApi\Model;
+
+final class Link
+{
+ use ExtensionTrait;
+
+ private $operationId;
+ private $parameters;
+ private $requestBody;
+ private $description;
+ private $server;
+
+ public function __construct(string $operationId, \ArrayObject $parameters = null, $requestBody = null, string $description = '', Server $server = null)
+ {
+ $this->operationId = $operationId;
+ $this->parameters = $parameters;
+ $this->requestBody = $requestBody;
+ $this->description = $description;
+ $this->server = $server;
+ }
+
+ public function getOperationId(): string
+ {
+ return $this->operationId;
+ }
+
+ public function getParameters(): \ArrayObject
+ {
+ return $this->parameters;
+ }
+
+ public function getRequestBody()
+ {
+ return $this->requestBody;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getServer(): ?Server
+ {
+ return $this->server;
+ }
+
+ public function withOperationId(string $operationId): self
+ {
+ $clone = clone $this;
+ $clone->operationId = $operationId;
+
+ return $clone;
+ }
+
+ public function withParameters(\ArrayObject $parameters): self
+ {
+ $clone = clone $this;
+ $clone->parameters = $parameters;
+
+ return $clone;
+ }
+
+ public function withRequestBody($requestBody): self
+ {
+ $clone = clone $this;
+ $clone->requestBody = $requestBody;
+
+ return $clone;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withServer(Server $server): self
+ {
+ $clone = clone $this;
+ $clone->server = $server;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/MediaType.php b/src/OpenApi/Model/MediaType.php
new file mode 100644
index 00000000000..8f232bb4b4b
--- /dev/null
+++ b/src/OpenApi/Model/MediaType.php
@@ -0,0 +1,84 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class MediaType
+{
+ use ExtensionTrait;
+
+ private $schema;
+ private $example;
+ private $examples;
+ private $encoding;
+
+ public function __construct(\ArrayObject $schema = null, $example = null, \ArrayObject $examples = null, Encoding $encoding = null)
+ {
+ $this->schema = $schema;
+ $this->example = $example;
+ $this->examples = $examples;
+ $this->encoding = $encoding;
+ }
+
+ public function getSchema(): ?\ArrayObject
+ {
+ return $this->schema;
+ }
+
+ public function getExample()
+ {
+ return $this->example;
+ }
+
+ public function getExamples(): ?\ArrayObject
+ {
+ return $this->examples;
+ }
+
+ public function getEncoding(): ?Encoding
+ {
+ return $this->encoding;
+ }
+
+ public function withSchema(\ArrayObject $schema): self
+ {
+ $clone = clone $this;
+ $clone->schema = $schema;
+
+ return $clone;
+ }
+
+ public function withExample($example): self
+ {
+ $clone = clone $this;
+ $clone->example = $example;
+
+ return $clone;
+ }
+
+ public function withExamples(\ArrayObject $examples): self
+ {
+ $clone = clone $this;
+ $clone->examples = $examples;
+
+ return $clone;
+ }
+
+ public function withEncoding(Encoding $encoding): self
+ {
+ $clone = clone $this;
+ $clone->encoding = $encoding;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/OAuthFlow.php b/src/OpenApi/Model/OAuthFlow.php
new file mode 100644
index 00000000000..1b3dfd4849b
--- /dev/null
+++ b/src/OpenApi/Model/OAuthFlow.php
@@ -0,0 +1,84 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class OAuthFlow
+{
+ use ExtensionTrait;
+
+ private $authorizationUrl;
+ private $tokenUrl;
+ private $refreshUrl;
+ private $scopes;
+
+ public function __construct(string $authorizationUrl = null, string $tokenUrl = null, string $refreshUrl = null, \ArrayObject $scopes = null)
+ {
+ $this->authorizationUrl = $authorizationUrl;
+ $this->tokenUrl = $tokenUrl;
+ $this->refreshUrl = $refreshUrl;
+ $this->scopes = $scopes;
+ }
+
+ public function getAuthorizationUrl(): ?string
+ {
+ return $this->authorizationUrl;
+ }
+
+ public function getTokenUrl(): ?string
+ {
+ return $this->tokenUrl;
+ }
+
+ public function getRefreshUrl(): ?string
+ {
+ return $this->refreshUrl;
+ }
+
+ public function getScopes(): \ArrayObject
+ {
+ return $this->scopes;
+ }
+
+ public function withAuthorizationUrl(string $authorizationUrl): self
+ {
+ $clone = clone $this;
+ $clone->authorizationUrl = $authorizationUrl;
+
+ return $clone;
+ }
+
+ public function withTokenUrl(string $tokenUrl): self
+ {
+ $clone = clone $this;
+ $clone->tokenUrl = $tokenUrl;
+
+ return $clone;
+ }
+
+ public function withRefreshUrl(string $refreshUrl): self
+ {
+ $clone = clone $this;
+ $clone->refreshUrl = $refreshUrl;
+
+ return $clone;
+ }
+
+ public function withScopes(\ArrayObject $scopes): self
+ {
+ $clone = clone $this;
+ $clone->scopes = $scopes;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/OAuthFlows.php b/src/OpenApi/Model/OAuthFlows.php
new file mode 100644
index 00000000000..afe06ad4824
--- /dev/null
+++ b/src/OpenApi/Model/OAuthFlows.php
@@ -0,0 +1,84 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class OAuthFlows
+{
+ use ExtensionTrait;
+
+ private $implicit;
+ private $password;
+ private $clientCredentials;
+ private $authorizationCode;
+
+ public function __construct(OAuthFlow $implicit = null, OAuthFlow $password = null, OAuthFlow $clientCredentials = null, OAuthFlow $authorizationCode = null)
+ {
+ $this->implicit = $implicit;
+ $this->password = $password;
+ $this->clientCredentials = $clientCredentials;
+ $this->authorizationCode = $authorizationCode;
+ }
+
+ public function getImplicit(): ?OAuthFlow
+ {
+ return $this->implicit;
+ }
+
+ public function getPassword(): ?OAuthFlow
+ {
+ return $this->password;
+ }
+
+ public function getClientCredentials(): ?OAuthFlow
+ {
+ return $this->clientCredentials;
+ }
+
+ public function getAuthorizationCode(): ?OAuthFlow
+ {
+ return $this->authorizationCode;
+ }
+
+ public function withImplicit(OAuthFlow $implicit): self
+ {
+ $clone = clone $this;
+ $clone->implicit = $implicit;
+
+ return $clone;
+ }
+
+ public function withPassword(OAuthFlow $password): self
+ {
+ $clone = clone $this;
+ $clone->password = $password;
+
+ return $clone;
+ }
+
+ public function withClientCredentials(OAuthFlow $clientCredentials): self
+ {
+ $clone = clone $this;
+ $clone->clientCredentials = $clientCredentials;
+
+ return $clone;
+ }
+
+ public function withAuthorizationCode(OAuthFlow $authorizationCode): self
+ {
+ $clone = clone $this;
+ $clone->authorizationCode = $authorizationCode;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Operation.php b/src/OpenApi/Model/Operation.php
new file mode 100644
index 00000000000..c5b7fd76faf
--- /dev/null
+++ b/src/OpenApi/Model/Operation.php
@@ -0,0 +1,211 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Operation
+{
+ use ExtensionTrait;
+
+ private $tags;
+ private $summary;
+ private $description;
+ private $operationId;
+ private $parameters;
+ private $requestBody;
+ private $responses;
+ private $callbacks;
+ private $deprecated;
+ private $security;
+ private $servers;
+ private $externalDocs;
+
+ public function __construct(string $operationId = null, array $tags = [], array $responses = [], string $summary = '', string $description = '', ExternalDocumentation $externalDocs = null, array $parameters = [], RequestBody $requestBody = null, \ArrayObject $callbacks = null, bool $deprecated = false, array $security = [], array $servers = [])
+ {
+ $this->tags = $tags;
+ $this->summary = $summary;
+ $this->description = $description;
+ $this->operationId = $operationId;
+ $this->parameters = $parameters;
+ $this->requestBody = $requestBody;
+ $this->responses = $responses;
+ $this->callbacks = $callbacks;
+ $this->deprecated = $deprecated;
+ $this->security = $security;
+ $this->servers = $servers;
+ $this->externalDocs = $externalDocs;
+ }
+
+ public function addResponse(Response $response, $status = 'default'): self
+ {
+ $this->responses[$status] = $response;
+
+ return $this;
+ }
+
+ public function getOperationId(): string
+ {
+ return $this->operationId;
+ }
+
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+
+ public function getResponses(): array
+ {
+ return $this->responses;
+ }
+
+ public function getSummary(): string
+ {
+ return $this->summary;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getExternalDocs(): ?ExternalDocumentation
+ {
+ return $this->externalDocs;
+ }
+
+ public function getParameters(): array
+ {
+ return $this->parameters;
+ }
+
+ public function getRequestBody(): ?RequestBody
+ {
+ return $this->requestBody;
+ }
+
+ public function getCallbacks(): ?\ArrayObject
+ {
+ return $this->callbacks;
+ }
+
+ public function getDeprecated(): bool
+ {
+ return $this->deprecated;
+ }
+
+ public function getSecurity(): array
+ {
+ return $this->security;
+ }
+
+ public function getServers(): array
+ {
+ return $this->servers;
+ }
+
+ public function withOperationId(string $operationId): self
+ {
+ $clone = clone $this;
+ $clone->operationId = $operationId;
+
+ return $clone;
+ }
+
+ public function withTags(array $tags): self
+ {
+ $clone = clone $this;
+ $clone->tags = $tags;
+
+ return $clone;
+ }
+
+ public function withResponses(array $responses): self
+ {
+ $clone = clone $this;
+ $clone->responses = $responses;
+
+ return $clone;
+ }
+
+ public function withSummary(string $summary): self
+ {
+ $clone = clone $this;
+ $clone->summary = $summary;
+
+ return $clone;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withExternalDocs(ExternalDocumentation $externalDocs): self
+ {
+ $clone = clone $this;
+ $clone->externalDocs = $externalDocs;
+
+ return $clone;
+ }
+
+ public function withParameters(array $parameters): self
+ {
+ $clone = clone $this;
+ $clone->parameters = $parameters;
+
+ return $clone;
+ }
+
+ public function withRequestBody(RequestBody $requestBody): self
+ {
+ $clone = clone $this;
+ $clone->requestBody = $requestBody;
+
+ return $clone;
+ }
+
+ public function withCallbacks(\ArrayObject $callbacks): self
+ {
+ $clone = clone $this;
+ $clone->callbacks = $callbacks;
+
+ return $clone;
+ }
+
+ public function withDeprecated(bool $deprecated): self
+ {
+ $clone = clone $this;
+ $clone->deprecated = $deprecated;
+
+ return $clone;
+ }
+
+ public function withSecurity(array $security): self
+ {
+ $clone = clone $this;
+ $clone->security = $security;
+
+ return $clone;
+ }
+
+ public function withServers(array $servers): self
+ {
+ $clone = clone $this;
+ $clone->servers = $servers;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Parameter.php b/src/OpenApi/Model/Parameter.php
new file mode 100644
index 00000000000..f68f0cbd231
--- /dev/null
+++ b/src/OpenApi/Model/Parameter.php
@@ -0,0 +1,227 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Parameter
+{
+ use ExtensionTrait;
+
+ private $name;
+ private $in;
+ private $description;
+ private $required;
+ private $deprecated;
+ private $allowEmptyValue;
+ private $schema;
+ private $explode;
+ private $allowReserved;
+ private $style;
+ private $example;
+ private $examples;
+ private $content;
+
+ public function __construct(string $name, string $in, string $description = '', bool $required = false, bool $deprecated = false, bool $allowEmptyValue = false, array $schema = [], string $style = null, bool $explode = false, bool $allowReserved = false, $example = null, \ArrayObject $examples = null, \ArrayObject $content = null)
+ {
+ $this->name = $name;
+ $this->in = $in;
+ $this->description = $description;
+ $this->required = $required;
+ $this->deprecated = $deprecated;
+ $this->allowEmptyValue = $allowEmptyValue;
+ $this->schema = $schema;
+ $this->explode = $explode;
+ $this->allowReserved = $allowReserved;
+ $this->example = $example;
+ $this->examples = $examples;
+ $this->content = $content;
+ $this->style = $style;
+
+ if (null === $style) {
+ if ('query' === $in || 'cookie' === $in) {
+ $this->style = 'form';
+ } elseif ('path' === $in || 'header' === $in) {
+ $this->style = 'simple';
+ }
+ }
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function getIn(): ?string
+ {
+ return $this->in;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getRequired(): bool
+ {
+ return $this->required;
+ }
+
+ public function getDeprecated(): bool
+ {
+ return $this->deprecated;
+ }
+
+ public function canAllowEmptyValue(): bool
+ {
+ return $this->allowEmptyValue;
+ }
+
+ public function getSchema(): array
+ {
+ return $this->schema;
+ }
+
+ public function getStyle(): string
+ {
+ return $this->style;
+ }
+
+ public function canExplode(): bool
+ {
+ return $this->explode;
+ }
+
+ public function canAllowReserved(): bool
+ {
+ return $this->allowReserved;
+ }
+
+ public function getExample()
+ {
+ return $this->example;
+ }
+
+ public function getExamples(): ?\ArrayObject
+ {
+ return $this->examples;
+ }
+
+ public function getContent(): ?\ArrayObject
+ {
+ return $this->content;
+ }
+
+ public function withName(string $name): self
+ {
+ $clone = clone $this;
+ $clone->name = $name;
+
+ return $clone;
+ }
+
+ public function withIn(string $in): self
+ {
+ $clone = clone $this;
+ $clone->in = $in;
+
+ return $clone;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withRequired(bool $required): self
+ {
+ $clone = clone $this;
+ $clone->required = $required;
+
+ return $clone;
+ }
+
+ public function withDeprecated(bool $deprecated): self
+ {
+ $clone = clone $this;
+ $clone->deprecated = $deprecated;
+
+ return $clone;
+ }
+
+ public function withAllowEmptyValue(bool $allowEmptyValue): self
+ {
+ $clone = clone $this;
+ $clone->allowEmptyValue = $allowEmptyValue;
+
+ return $clone;
+ }
+
+ public function withSchema(array $schema): self
+ {
+ $clone = clone $this;
+ $clone->schema = $schema;
+
+ return $clone;
+ }
+
+ public function withStyle(string $style): self
+ {
+ $clone = clone $this;
+ $clone->style = $style;
+
+ return $clone;
+ }
+
+ public function withExplode(bool $explode): self
+ {
+ $clone = clone $this;
+ $clone->explode = $explode;
+
+ return $clone;
+ }
+
+ public function withAllowReserved(bool $allowReserved): self
+ {
+ $clone = clone $this;
+ $clone->allowReserved = $allowReserved;
+
+ return $clone;
+ }
+
+ public function withExample($example): self
+ {
+ $clone = clone $this;
+ $clone->example = $example;
+
+ return $clone;
+ }
+
+ public function withExamples(\ArrayObject $examples): self
+ {
+ $clone = clone $this;
+ $clone->examples = $examples;
+
+ return $clone;
+ }
+
+ public function withContent(\ArrayObject $content): self
+ {
+ $clone = clone $this;
+ $clone->content = $content;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/PathItem.php b/src/OpenApi/Model/PathItem.php
new file mode 100644
index 00000000000..1285336fa74
--- /dev/null
+++ b/src/OpenApi/Model/PathItem.php
@@ -0,0 +1,220 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class PathItem
+{
+ use ExtensionTrait;
+
+ private static $methods = ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE'];
+ private $ref;
+ private $summary;
+ private $description;
+ private $get;
+ private $put;
+ private $post;
+ private $delete;
+ private $options;
+ private $head;
+ private $patch;
+ private $trace;
+ private $servers;
+ private $parameters;
+
+ public function __construct(string $ref = null, string $summary = null, string $description = null, Operation $get = null, Operation $put = null, Operation $post = null, Operation $delete = null, Operation $options = null, Operation $head = null, Operation $patch = null, Operation $trace = null, array $servers = [], array $parameters = [])
+ {
+ $this->ref = $ref;
+ $this->summary = $summary;
+ $this->description = $description;
+ $this->get = $get;
+ $this->put = $put;
+ $this->post = $post;
+ $this->delete = $delete;
+ $this->options = $options;
+ $this->head = $head;
+ $this->patch = $patch;
+ $this->trace = $trace;
+ $this->servers = $servers;
+ $this->parameters = $parameters;
+ }
+
+ public function getRef(): ?string
+ {
+ return $this->ref;
+ }
+
+ public function getSummary(): ?string
+ {
+ return $this->summary;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function getGet(): ?Operation
+ {
+ return $this->get;
+ }
+
+ public function getPut(): ?Operation
+ {
+ return $this->put;
+ }
+
+ public function getPost(): ?Operation
+ {
+ return $this->post;
+ }
+
+ public function getDelete(): ?Operation
+ {
+ return $this->delete;
+ }
+
+ public function getOptions(): ?Operation
+ {
+ return $this->options;
+ }
+
+ public function getHead(): ?Operation
+ {
+ return $this->head;
+ }
+
+ public function getPatch(): ?Operation
+ {
+ return $this->patch;
+ }
+
+ public function getTrace(): ?Operation
+ {
+ return $this->trace;
+ }
+
+ public function getServers(): array
+ {
+ return $this->servers;
+ }
+
+ public function getParameters(): array
+ {
+ return $this->parameters;
+ }
+
+ public function withRef(string $ref): self
+ {
+ $clone = clone $this;
+ $clone->ref = $ref;
+
+ return $clone;
+ }
+
+ public function withSummary(string $summary): self
+ {
+ $clone = clone $this;
+ $clone->summary = $summary;
+
+ return $clone;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withGet(Operation $get): self
+ {
+ $clone = clone $this;
+ $clone->get = $get;
+
+ return $clone;
+ }
+
+ public function withPut(Operation $put): self
+ {
+ $clone = clone $this;
+ $clone->put = $put;
+
+ return $clone;
+ }
+
+ public function withPost(Operation $post): self
+ {
+ $clone = clone $this;
+ $clone->post = $post;
+
+ return $clone;
+ }
+
+ public function withDelete(Operation $delete): self
+ {
+ $clone = clone $this;
+ $clone->delete = $delete;
+
+ return $clone;
+ }
+
+ public function withOptions(Operation $options): self
+ {
+ $clone = clone $this;
+ $clone->options = $options;
+
+ return $clone;
+ }
+
+ public function withHead(Operation $head): self
+ {
+ $clone = clone $this;
+ $clone->head = $head;
+
+ return $clone;
+ }
+
+ public function withPatch(Operation $patch): self
+ {
+ $clone = clone $this;
+ $clone->patch = $patch;
+
+ return $clone;
+ }
+
+ public function withTrace(Operation $trace): self
+ {
+ $clone = clone $this;
+ $clone->trace = $trace;
+
+ return $clone;
+ }
+
+ public function withServers(array $servers): self
+ {
+ $clone = clone $this;
+ $clone->servers = $servers;
+
+ return $clone;
+ }
+
+ public function withParameters(array $parameters): self
+ {
+ $clone = clone $this;
+ $clone->parameters = $parameters;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Paths.php b/src/OpenApi/Model/Paths.php
new file mode 100644
index 00000000000..c0b30a9a64c
--- /dev/null
+++ b/src/OpenApi/Model/Paths.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\OpenApi\Model;
+
+final class Paths
+{
+ private $paths;
+
+ public function addPath(string $path, PathItem $pathItem)
+ {
+ $this->paths[$path] = $pathItem;
+ }
+
+ public function getPath(string $path): ?PathItem
+ {
+ return $this->paths[$path] ?? null;
+ }
+
+ public function getPaths(): array
+ {
+ return $this->paths;
+ }
+}
diff --git a/src/OpenApi/Model/RequestBody.php b/src/OpenApi/Model/RequestBody.php
new file mode 100644
index 00000000000..04235777a5e
--- /dev/null
+++ b/src/OpenApi/Model/RequestBody.php
@@ -0,0 +1,69 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class RequestBody
+{
+ use ExtensionTrait;
+
+ private $description;
+ private $content;
+ private $required;
+
+ public function __construct(string $description = '', \ArrayObject $content = null, bool $required = false)
+ {
+ $this->description = $description;
+ $this->content = $content;
+ $this->required = $required;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getContent(): \ArrayObject
+ {
+ return $this->content;
+ }
+
+ public function getRequired(): bool
+ {
+ return $this->required;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withContent(\ArrayObject $content): self
+ {
+ $clone = clone $this;
+ $clone->content = $content;
+
+ return $clone;
+ }
+
+ public function withRequired(bool $required): self
+ {
+ $clone = clone $this;
+ $clone->required = $required;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Response.php b/src/OpenApi/Model/Response.php
new file mode 100644
index 00000000000..9bb2f84d353
--- /dev/null
+++ b/src/OpenApi/Model/Response.php
@@ -0,0 +1,84 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Response
+{
+ use ExtensionTrait;
+
+ private $description;
+ private $content;
+ private $headers;
+ private $links;
+
+ public function __construct(string $description = '', \ArrayObject $content = null, \ArrayObject $headers = null, \ArrayObject $links = null)
+ {
+ $this->description = $description;
+ $this->content = $content;
+ $this->headers = $headers;
+ $this->links = $links;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getContent(): ?\ArrayObject
+ {
+ return $this->content;
+ }
+
+ public function getHeaders(): ?\ArrayObject
+ {
+ return $this->headers;
+ }
+
+ public function getLinks(): ?\ArrayObject
+ {
+ return $this->links;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withContent(\ArrayObject $content): self
+ {
+ $clone = clone $this;
+ $clone->content = $content;
+
+ return $clone;
+ }
+
+ public function withHeaders(\ArrayObject $headers): self
+ {
+ $clone = clone $this;
+ $clone->headers = $headers;
+
+ return $clone;
+ }
+
+ public function withLinks(\ArrayObject $links): self
+ {
+ $clone = clone $this;
+ $clone->links = $links;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Schema.php b/src/OpenApi/Model/Schema.php
new file mode 100644
index 00000000000..5b9060238f7
--- /dev/null
+++ b/src/OpenApi/Model/Schema.php
@@ -0,0 +1,171 @@
+
+ *
+ * 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\OpenApi\Model;
+
+use ApiPlatform\Core\JsonSchema\Schema as JsonSchema;
+
+final class Schema extends \ArrayObject
+{
+ use ExtensionTrait;
+
+ private $nullable;
+ private $discriminator;
+ private $readOnly;
+ private $writeOnly;
+ private $xml;
+ private $externalDocs;
+ private $example;
+ private $deprecated;
+ private $schema;
+
+ public function __construct(bool $nullable = false, $discriminator = null, bool $readOnly = false, bool $writeOnly = false, string $xml = null, $externalDocs = null, $example = null, bool $deprecated = false)
+ {
+ $this->nullable = $nullable;
+ $this->discriminator = $discriminator;
+ $this->readOnly = $readOnly;
+ $this->writeOnly = $writeOnly;
+ $this->xml = $xml;
+ $this->externalDocs = $externalDocs;
+ $this->example = $example;
+ $this->deprecated = $deprecated;
+ $this->schema = new JsonSchema();
+
+ parent::__construct([]);
+ }
+
+ public function setDefinitions(array $definitions)
+ {
+ $this->schema->setDefinitions(new \ArrayObject($definitions));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getArrayCopy(): array
+ {
+ $schema = parent::getArrayCopy();
+ unset($schema['schema']);
+
+ return $schema;
+ }
+
+ public function getDefinitions(): \ArrayObject
+ {
+ return new \ArrayObject(array_merge($this->schema->getArrayCopy(true), $this->getArrayCopy()));
+ }
+
+ public function getNullable(): bool
+ {
+ return $this->nullable;
+ }
+
+ public function getDiscriminator()
+ {
+ return $this->discriminator;
+ }
+
+ public function getReadOnly(): bool
+ {
+ return $this->readOnly;
+ }
+
+ public function getWriteOnly(): bool
+ {
+ return $this->writeOnly;
+ }
+
+ public function getXml(): string
+ {
+ return $this->xml;
+ }
+
+ public function getExternalDocs()
+ {
+ return $this->externalDocs;
+ }
+
+ public function getExample()
+ {
+ return $this->example;
+ }
+
+ public function getDeprecated(): bool
+ {
+ return $this->deprecated;
+ }
+
+ public function withNullable(bool $nullable): self
+ {
+ $clone = clone $this;
+ $clone->nullable = $nullable;
+
+ return $clone;
+ }
+
+ public function withDiscriminator($discriminator): self
+ {
+ $clone = clone $this;
+ $clone->discriminator = $discriminator;
+
+ return $clone;
+ }
+
+ public function withReadOnly(bool $readOnly): self
+ {
+ $clone = clone $this;
+ $clone->readOnly = $readOnly;
+
+ return $clone;
+ }
+
+ public function withWriteOnly(bool $writeOnly): self
+ {
+ $clone = clone $this;
+ $clone->writeOnly = $writeOnly;
+
+ return $clone;
+ }
+
+ public function withXml(string $xml): self
+ {
+ $clone = clone $this;
+ $clone->xml = $xml;
+
+ return $clone;
+ }
+
+ public function withExternalDocs($externalDocs): self
+ {
+ $clone = clone $this;
+ $clone->externalDocs = $externalDocs;
+
+ return $clone;
+ }
+
+ public function withExample($example): self
+ {
+ $clone = clone $this;
+ $clone->example = $example;
+
+ return $clone;
+ }
+
+ public function withDeprecated(bool $deprecated): self
+ {
+ $clone = clone $this;
+ $clone->deprecated = $deprecated;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/SecurityScheme.php b/src/OpenApi/Model/SecurityScheme.php
new file mode 100644
index 00000000000..385e4e028c5
--- /dev/null
+++ b/src/OpenApi/Model/SecurityScheme.php
@@ -0,0 +1,144 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class SecurityScheme
+{
+ use ExtensionTrait;
+
+ private $type;
+ private $description;
+ private $name;
+ private $in;
+ private $scheme;
+ private $bearerFormat;
+ private $flows;
+ private $openIdConnectUrl;
+
+ public function __construct(string $type = null, string $description = '', string $name = null, string $in = null, string $scheme = null, string $bearerFormat = null, OAuthFlows $flows = null, string $openIdConnectUrl = null)
+ {
+ $this->type = $type;
+ $this->description = $description;
+ $this->name = $name;
+ $this->in = $in;
+ $this->scheme = $scheme;
+ $this->bearerFormat = $bearerFormat;
+ $this->flows = $flows;
+ $this->openIdConnectUrl = $openIdConnectUrl;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function getIn(): ?string
+ {
+ return $this->in;
+ }
+
+ public function getScheme(): string
+ {
+ return $this->scheme;
+ }
+
+ public function getBearerFormat(): ?string
+ {
+ return $this->bearerFormat;
+ }
+
+ public function getFlows(): ?OAuthFlows
+ {
+ return $this->flows;
+ }
+
+ public function getOpenIdConnectUrl(): ?string
+ {
+ return $this->openIdConnectUrl;
+ }
+
+ public function withType(string $type): self
+ {
+ $clone = clone $this;
+ $clone->type = $type;
+
+ return $clone;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withName(string $name): self
+ {
+ $clone = clone $this;
+ $clone->name = $name;
+
+ return $clone;
+ }
+
+ public function withIn(string $in): self
+ {
+ $clone = clone $this;
+ $clone->in = $in;
+
+ return $clone;
+ }
+
+ public function withScheme(string $scheme): self
+ {
+ $clone = clone $this;
+ $clone->scheme = $scheme;
+
+ return $clone;
+ }
+
+ public function withBearerFormat(string $bearerFormat): self
+ {
+ $clone = clone $this;
+ $clone->bearerFormat = $bearerFormat;
+
+ return $clone;
+ }
+
+ public function withFlows(OAuthFlows $flows): self
+ {
+ $clone = clone $this;
+ $clone->flows = $flows;
+
+ return $clone;
+ }
+
+ public function withOpenIdConnectUrl(string $openIdConnectUrl): self
+ {
+ $clone = clone $this;
+ $clone->openIdConnectUrl = $openIdConnectUrl;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Model/Server.php b/src/OpenApi/Model/Server.php
new file mode 100644
index 00000000000..852afd5c9ad
--- /dev/null
+++ b/src/OpenApi/Model/Server.php
@@ -0,0 +1,69 @@
+
+ *
+ * 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\OpenApi\Model;
+
+final class Server
+{
+ use ExtensionTrait;
+
+ private $url;
+ private $description;
+ private $variables;
+
+ public function __construct(string $url, string $description = '', \ArrayObject $variables = null)
+ {
+ $this->url = $url;
+ $this->description = $description;
+ $this->variables = $variables;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getVariables(): ?\ArrayObject
+ {
+ return $this->variables;
+ }
+
+ public function withUrl(string $url): self
+ {
+ $clone = clone $this;
+ $clone->url = $url;
+
+ return $clone;
+ }
+
+ public function withDescription(string $description): self
+ {
+ $clone = clone $this;
+ $clone->description = $description;
+
+ return $clone;
+ }
+
+ public function withVariables(\ArrayObject $variables): self
+ {
+ $clone = clone $this;
+ $clone->variables = $variables;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/OpenApi.php b/src/OpenApi/OpenApi.php
new file mode 100644
index 00000000000..94c50377671
--- /dev/null
+++ b/src/OpenApi/OpenApi.php
@@ -0,0 +1,152 @@
+
+ *
+ * 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\OpenApi;
+
+use ApiPlatform\Core\Documentation\DocumentationInterface;
+use ApiPlatform\Core\OpenApi\Model\Components;
+use ApiPlatform\Core\OpenApi\Model\ExtensionTrait;
+use ApiPlatform\Core\OpenApi\Model\Info;
+use ApiPlatform\Core\OpenApi\Model\Paths;
+
+final class OpenApi implements DocumentationInterface
+{
+ use ExtensionTrait;
+
+ public const VERSION = '3.0.3';
+
+ private $openapi;
+ private $info;
+ private $servers;
+ private $paths;
+ private $components;
+ private $security;
+ private $tags;
+ private $externalDocs;
+
+ public function __construct(Info $info, array $servers = [], Paths $paths, Components $components = null, array $security = [], array $tags = [], $externalDocs = null)
+ {
+ $this->openapi = self::VERSION;
+ $this->info = $info;
+ $this->servers = $servers;
+ $this->paths = $paths;
+ $this->components = $components;
+ $this->security = $security;
+ $this->tags = $tags;
+ $this->externalDocs = $externalDocs;
+ }
+
+ public function getOpenapi(): string
+ {
+ return $this->openapi;
+ }
+
+ public function getInfo(): Info
+ {
+ return $this->info;
+ }
+
+ public function getServers(): array
+ {
+ return $this->servers;
+ }
+
+ public function getPaths()
+ {
+ return $this->paths;
+ }
+
+ public function getComponents(): Components
+ {
+ return $this->components;
+ }
+
+ public function getSecurity(): array
+ {
+ return $this->security;
+ }
+
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+
+ public function getExternalDocs(): ?array
+ {
+ return $this->externalDocs;
+ }
+
+ public function withOpenapi(string $openapi): self
+ {
+ $clone = clone $this;
+ $clone->openapi = $openapi;
+
+ return $clone;
+ }
+
+ public function withInfo(Info $info): self
+ {
+ $clone = clone $this;
+ $clone->info = $info;
+
+ return $clone;
+ }
+
+ public function withServers(array $servers): self
+ {
+ $clone = clone $this;
+ $clone->servers = $servers;
+
+ return $clone;
+ }
+
+ public function withPaths(Paths $paths): self
+ {
+ $clone = clone $this;
+ $clone->paths = $paths;
+
+ return $clone;
+ }
+
+ public function withComponents(Components $components): self
+ {
+ $clone = clone $this;
+ $clone->components = $components;
+
+ return $clone;
+ }
+
+ public function withSecurity(array $security): self
+ {
+ $clone = clone $this;
+ $clone->security = $security;
+
+ return $clone;
+ }
+
+ public function withTags(array $tags): self
+ {
+ $clone = clone $this;
+ $clone->tags = $tags;
+
+ return $clone;
+ }
+
+ public function withExternalDocs(array $externalDocs): self
+ {
+ $clone = clone $this;
+ $clone->externalDocs = $externalDocs;
+
+ return $clone;
+ }
+}
diff --git a/src/OpenApi/Options.php b/src/OpenApi/Options.php
new file mode 100644
index 00000000000..c1b35e2e719
--- /dev/null
+++ b/src/OpenApi/Options.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\OpenApi;
+
+final class Options
+{
+ private $title;
+ private $description;
+ private $version;
+ private $oAuthEnabled;
+ private $oAuthType;
+ private $oAuthFlow;
+ private $oAuthTokenUrl;
+ private $oAuthAuthorizationUrl;
+ private $oAuthRefreshUrl;
+ private $oAuthScopes;
+ private $apiKeys;
+
+ public function __construct(string $title, string $description = '', string $version = '', bool $oAuthEnabled = false, string $oAuthType = '', string $oAuthFlow = '', string $oAuthTokenUrl = '', string $oAuthAuthorizationUrl = '', string $oAuthRefreshUrl = '', array $oAuthScopes = [], array $apiKeys = [])
+ {
+ $this->title = $title;
+ $this->description = $description;
+ $this->version = $version;
+ $this->oAuthEnabled = $oAuthEnabled;
+ $this->oAuthType = $oAuthType;
+ $this->oAuthFlow = $oAuthFlow;
+ $this->oAuthTokenUrl = $oAuthTokenUrl;
+ $this->oAuthAuthorizationUrl = $oAuthAuthorizationUrl;
+ $this->oAuthRefreshUrl = $oAuthRefreshUrl;
+ $this->oAuthScopes = $oAuthScopes;
+ $this->apiKeys = $apiKeys;
+ }
+
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ public function getOAuthEnabled(): bool
+ {
+ return $this->oAuthEnabled;
+ }
+
+ public function getOAuthType(): string
+ {
+ return $this->oAuthType;
+ }
+
+ public function getOAuthFlow(): string
+ {
+ return $this->oAuthFlow;
+ }
+
+ public function getOAuthTokenUrl(): string
+ {
+ return $this->oAuthTokenUrl;
+ }
+
+ public function getOAuthAuthorizationUrl(): string
+ {
+ return $this->oAuthAuthorizationUrl;
+ }
+
+ public function getOAuthRefreshUrl(): string
+ {
+ return $this->oAuthRefreshUrl;
+ }
+
+ public function getOAuthScopes(): array
+ {
+ return $this->oAuthScopes;
+ }
+
+ public function getApiKeys(): array
+ {
+ return $this->apiKeys;
+ }
+}
diff --git a/src/OpenApi/Serializer/OpenApiNormalizer.php b/src/OpenApi/Serializer/OpenApiNormalizer.php
new file mode 100644
index 00000000000..975e340c14c
--- /dev/null
+++ b/src/OpenApi/Serializer/OpenApiNormalizer.php
@@ -0,0 +1,91 @@
+
+ *
+ * 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\OpenApi\Serializer;
+
+use ApiPlatform\Core\OpenApi\OpenApi;
+use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
+use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Generates an OpenAPI v3 specification.
+ */
+final class OpenApiNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
+{
+ public const FORMAT = 'json';
+ public const OPEN_API_PRESERVE_EMPTY_OBJECTS = 'open_api_preserve_empty_objects';
+ private const EXTENSION_PROPERTIES_KEY = 'extensionProperties';
+
+ private $decorated;
+
+ public function __construct(NormalizerInterface $decorated)
+ {
+ $this->decorated = $decorated;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize($object, $format = null, array $context = []): array
+ {
+ $context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] = true;
+ $context[AbstractObjectNormalizer::SKIP_NULL_VALUES] = true;
+
+ return $this->recursiveClean($this->decorated->normalize($object, $format, $context));
+ }
+
+ private function recursiveClean($data): array
+ {
+ foreach ($data as $key => $value) {
+ if (self::EXTENSION_PROPERTIES_KEY === $key) {
+ foreach ($data[self::EXTENSION_PROPERTIES_KEY] as $extensionPropertyKey => $extensionPropertyValue) {
+ $data[$extensionPropertyKey] = $extensionPropertyValue;
+ }
+ continue;
+ }
+
+ // Side effect of using getPaths(): Paths which itself contains the array
+ if ('paths' === $key) {
+ $value = $data['paths'] = $data['paths']['paths'];
+ unset($data['paths']['paths']);
+ }
+
+ if (\is_array($value)) {
+ $data[$key] = $this->recursiveClean($value);
+ // arrays must stay even if empty
+ continue;
+ }
+ }
+
+ unset($data[self::EXTENSION_PROPERTIES_KEY]);
+
+ return $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsNormalization($data, $format = null): bool
+ {
+ return self::FORMAT === $format && $data instanceof OpenApi;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheableSupportsMethod(): bool
+ {
+ return true;
+ }
+}
diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php
index 835fe75a740..8c88ad034ad 100644
--- a/src/Swagger/Serializer/DocumentationNormalizer.php
+++ b/src/Swagger/Serializer/DocumentationNormalizer.php
@@ -85,6 +85,7 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup
private $paginationClientEnabledParameterName;
private $formats;
private $formatsProvider;
+
/**
* @var SchemaFactoryInterface
*/
diff --git a/tests/Bridge/Symfony/Bundle/Command/OpenApiCommandTest.php b/tests/Bridge/Symfony/Bundle/Command/OpenApiCommandTest.php
new file mode 100644
index 00000000000..a9df5225a0e
--- /dev/null
+++ b/tests/Bridge/Symfony/Bundle/Command/OpenApiCommandTest.php
@@ -0,0 +1,112 @@
+
+ *
+ * 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\Bridge\Symfony\Bundle\Command;
+
+use ApiPlatform\Core\OpenApi\OpenApi;
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Symfony\Component\Console\Tester\ApplicationTester;
+use Symfony\Component\Yaml\Exception\ParseException;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * @author Amrouche Hamza
+ */
+class OpenApiCommandTest extends KernelTestCase
+{
+ /**
+ * @var ApplicationTester
+ */
+ private $tester;
+
+ protected function setUp(): void
+ {
+ self::bootKernel();
+
+ $application = new Application(static::$kernel);
+ $application->setCatchExceptions(false);
+ $application->setAutoExit(false);
+
+ $this->tester = new ApplicationTester($application);
+ }
+
+ public function testExecute()
+ {
+ $this->tester->run(['command' => 'api:openapi:export']);
+
+ $this->assertJson($this->tester->getDisplay());
+ }
+
+ public function testExecuteWithYaml()
+ {
+ $this->tester->run(['command' => 'api:openapi:export', '--yaml' => true]);
+
+ $result = $this->tester->getDisplay();
+ $this->assertYaml($result);
+
+ $expected = <<assertStringContainsString($expected, $result, 'nested object should be present.');
+
+ $expected = <<assertStringContainsString($expected, $result, 'arrays should be correctly formatted.');
+ $this->assertStringContainsString('openapi: '.OpenApi::VERSION, $result);
+
+ $expected = <<assertStringContainsString($expected, $result, 'multiline formatting must be preserved (using literal style).');
+ }
+
+ public function testWriteToFile()
+ {
+ /** @var string $tmpFile */
+ $tmpFile = tempnam(sys_get_temp_dir(), 'test_write_to_file');
+
+ $this->tester->run(['command' => 'api:openapi:export', '--output' => $tmpFile]);
+
+ $this->assertJson((string) @file_get_contents($tmpFile));
+ @unlink($tmpFile);
+ }
+
+ /**
+ * @param string $data
+ */
+ private function assertYaml($data)
+ {
+ try {
+ Yaml::parse($data);
+ } catch (ParseException $exception) {
+ $this->fail('Is not valid YAML: '.$exception->getMessage());
+ }
+ $this->addToAssertionCount(1);
+ }
+}
diff --git a/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php b/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php
index e290d87768c..69ba05ee03d 100644
--- a/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php
+++ b/tests/Bridge/Symfony/Bundle/Command/SwaggerCommandTest.php
@@ -41,6 +41,10 @@ protected function setUp(): void
$this->tester = new ApplicationTester($application);
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The command "api:swagger:export" is deprecated for the spec version 3 use "api:openapi:export".
+ */
public function testExecuteWithAliasVersion3()
{
$this->tester->run(['command' => 'api:swagger:export', '--spec-version' => 3]);
@@ -48,13 +52,10 @@ public function testExecuteWithAliasVersion3()
$this->assertJson($this->tester->getDisplay());
}
- public function testExecuteOpenApiVersion2()
- {
- $this->tester->run(['command' => 'api:openapi:export']);
-
- $this->assertJson($this->tester->getDisplay());
- }
-
+ /**
+ * @group legacy
+ * @expectedDeprecation The command "api:swagger:export" is deprecated for the spec version 3 use "api:openapi:export".
+ */
public function testExecuteWithYamlVersion3()
{
$this->tester->run(['command' => 'api:swagger:export', '--yaml' => true, '--spec-version' => 3]);
@@ -93,20 +94,11 @@ public function testExecuteWithYamlVersion3()
$this->assertStringContainsString($expected, $result, 'multiline formatting must be preserved (using literal style).');
}
- public function testExecuteOpenApiVersion2WithYaml()
- {
- $this->tester->run(['command' => 'api:openapi:export', '--yaml' => true]);
-
- $result = $this->tester->getDisplay();
- $this->assertYaml($result);
- $this->assertStringContainsString("swagger: '2.0'", $result);
- }
-
public function testExecuteWithBadArguments()
{
$this->expectException(InvalidOptionException::class);
$this->expectExceptionMessage('This tool only supports versions 2, 3 of the OpenAPI specification ("foo" given).');
- $this->tester->run(['command' => 'api:openapi:export', '--spec-version' => 'foo', '--yaml' => true]);
+ $this->tester->run(['command' => 'api:swagger:export', '--spec-version' => 'foo', '--yaml' => true]);
}
public function testWriteToFile()
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index fd3ad6ee7fe..2271044208c 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -61,6 +61,7 @@
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\Pagination;
+use ApiPlatform\Core\DataProvider\PaginationOptions;
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Exception\FilterValidationException;
@@ -77,6 +78,9 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
+use ApiPlatform\Core\OpenApi\Options;
+use ApiPlatform\Core\OpenApi\Serializer\OpenApiNormalizer;
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
use ApiPlatform\Core\Serializer\Filter\GroupFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
@@ -960,6 +964,7 @@ private function getPartialContainerBuilderProphecy($configuration = null)
'api_platform.operation_path_resolver.generator',
'api_platform.operation_path_resolver.underscore',
'api_platform.pagination',
+ 'api_platform.pagination_options',
'api_platform.path_segment_name_generator.underscore',
'api_platform.path_segment_name_generator.dash',
'api_platform.resource_class_resolver',
@@ -1019,6 +1024,7 @@ private function getPartialContainerBuilderProphecy($configuration = null)
SerializerContextBuilderInterface::class => 'api_platform.serializer.context_builder',
SubresourceDataProviderInterface::class => 'api_platform.subresource_data_provider',
UrlGeneratorInterface::class => 'api_platform.router',
+ PaginationOptions::class => 'api_platform.pagination_options',
];
foreach ($aliases as $alias => $service) {
@@ -1143,6 +1149,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
'api_platform.oauth.flow' => 'application',
'api_platform.oauth.tokenUrl' => '/oauth/v2/token',
'api_platform.oauth.authorizationUrl' => '/oauth/v2/auth',
+ 'api_platform.oauth.refreshUrl' => '/oauth/v2/refresh',
'api_platform.oauth.scopes' => [],
'api_platform.enable_swagger_ui' => true,
'api_platform.enable_re_doc' => true,
@@ -1319,6 +1326,11 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
$definitions[] = 'api_platform.json_schema.type_factory';
$definitions[] = 'api_platform.json_schema.schema_factory';
$definitions[] = 'api_platform.json_schema.json_schema_generate_command';
+ $definitions[] = 'api_platform.openapi.options';
+ $definitions[] = 'api_platform.openapi.normalizer';
+ $definitions[] = 'api_platform.openapi.normalizer.api_gateway';
+ $definitions[] = 'api_platform.openapi.factory';
+ $definitions[] = 'api_platform.openapi.command';
}
// has jsonld
@@ -1390,6 +1402,9 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
$aliases += [
TypeFactoryInterface::class => 'api_platform.json_schema.type_factory',
SchemaFactoryInterface::class => 'api_platform.json_schema.schema_factory',
+ Options::class => 'api_platform.openapi.options',
+ OpenApiNormalizer::class => 'api_platform.openapi.normalizer',
+ OpenApiFactoryInterface::class => 'api_platform.openapi.factory',
];
}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
index 57510a68ca1..2276679a860 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
@@ -144,6 +144,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
'flow' => 'application',
'tokenUrl' => '/oauth/v2/token',
'authorizationUrl' => '/oauth/v2/auth',
+ 'refreshUrl' => '/oauth/v2/refresh',
'scopes' => [],
],
'swagger' => [
diff --git a/tests/Documentation/Action/DocumentationActionTest.php b/tests/Documentation/Action/DocumentationActionTest.php
index 3d7bbd3efe7..728fd06a92e 100644
--- a/tests/Documentation/Action/DocumentationActionTest.php
+++ b/tests/Documentation/Action/DocumentationActionTest.php
@@ -18,6 +18,10 @@
use ApiPlatform\Core\Documentation\Documentation;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
+use ApiPlatform\Core\OpenApi\Model\Info;
+use ApiPlatform\Core\OpenApi\Model\Paths;
+use ApiPlatform\Core\OpenApi\OpenApi;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
@@ -29,7 +33,11 @@
*/
class DocumentationActionTest extends TestCase
{
- public function testyDocumentationAction(): void
+ /**
+ * @group legacy
+ * @expectedDeprecation Not passing an instance of "ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface" as 7th parameter of the constructor of "ApiPlatform\Core\Documentation\Action\DocumentationAction" is deprecated since API Platform 2.6
+ */
+ public function testDocumentationAction(): void
{
$requestProphecy = $this->prophesize(Request::class);
$attributesProphecy = $this->prophesize(ParameterBagInterface::class);
@@ -81,4 +89,48 @@ public function testDocumentationActionFormatDeprecation()
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies']));
new DocumentationAction($resourceNameCollectionFactoryProphecy->reveal(), '', '', '', ['formats' => ['jsonld' => 'application/ld+json']]);
}
+
+ public function testDocumentationActionV2(): void
+ {
+ $openApiFactoryProphecy = $this->prophesize(OpenApiFactoryInterface::class);
+ $requestProphecy = $this->prophesize(Request::class);
+ $attributesProphecy = $this->prophesize(ParameterBagInterface::class);
+ $queryProphecy = $this->prophesize(ParameterBag::class);
+ $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
+ $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies']));
+ $requestProphecy->attributes = $attributesProphecy->reveal();
+ $requestProphecy->query = $queryProphecy->reveal();
+ $requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1);
+ $queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1);
+ $queryProphecy->getInt('spec_version', 2)->willReturn(2)->shouldBeCalledTimes(1);
+ $attributesProphecy->all()->willReturn(['_api_normalization_context' => ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => 2]])->shouldBeCalledTimes(1);
+ $attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1);
+ $attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => 2])->shouldBeCalledTimes(1);
+
+ $documentation = new DocumentationAction($resourceNameCollectionFactoryProphecy->reveal(), 'My happy hippie api', 'lots of chocolate', '1.0.0', null, [2, 3], $openApiFactoryProphecy->reveal());
+ $this->assertEquals(new Documentation(new ResourceNameCollection(['dummies']), 'My happy hippie api', 'lots of chocolate', '1.0.0'), $documentation($requestProphecy->reveal()));
+ }
+
+ public function testDocumentationActionV3(): void
+ {
+ $openApi = new OpenApi(new Info('my api', '1.0.0'), [], new Paths());
+ $openApiFactoryProphecy = $this->prophesize(OpenApiFactoryInterface::class);
+ $openApiFactoryProphecy->__invoke(Argument::any())->shouldBeCalled()->willReturn($openApi);
+ $requestProphecy = $this->prophesize(Request::class);
+ $attributesProphecy = $this->prophesize(ParameterBagInterface::class);
+ $queryProphecy = $this->prophesize(ParameterBag::class);
+ $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
+ $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies']));
+ $requestProphecy->attributes = $attributesProphecy->reveal();
+ $requestProphecy->query = $queryProphecy->reveal();
+ $requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1);
+ $queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1);
+ $queryProphecy->getInt('spec_version', 2)->willReturn(3)->shouldBeCalledTimes(1);
+ $attributesProphecy->all()->willReturn(['_api_normalization_context' => ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => 3]])->shouldBeCalledTimes(1);
+ $attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1);
+ $attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => 3])->shouldBeCalledTimes(1);
+
+ $documentation = new DocumentationAction($resourceNameCollectionFactoryProphecy->reveal(), 'My happy hippie api', 'lots of chocolate', '1.0.0', null, [2, 3], $openApiFactoryProphecy->reveal());
+ $this->assertInstanceOf(OpenApi::class, $documentation($requestProphecy->reveal()));
+ }
}
diff --git a/tests/OpenApi/Factory/OpenApiFactoryTest.php b/tests/OpenApi/Factory/OpenApiFactoryTest.php
new file mode 100644
index 00000000000..f960dc3a113
--- /dev/null
+++ b/tests/OpenApi/Factory/OpenApiFactoryTest.php
@@ -0,0 +1,672 @@
+
+ *
+ * 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\OpenApi\Factory;
+
+use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver;
+use ApiPlatform\Core\DataProvider\PaginationOptions;
+use ApiPlatform\Core\JsonSchema\Schema;
+use ApiPlatform\Core\JsonSchema\SchemaFactory;
+use ApiPlatform\Core\JsonSchema\TypeFactory;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
+use ApiPlatform\Core\Metadata\Property\SubresourceMetadata;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactory;
+use ApiPlatform\Core\OpenApi\Model;
+use ApiPlatform\Core\OpenApi\OpenApi;
+use ApiPlatform\Core\OpenApi\Options;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
+use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator;
+use ApiPlatform\Core\PathResolver\CustomOperationPathResolver;
+use ApiPlatform\Core\PathResolver\OperationPathResolver;
+use ApiPlatform\Core\Tests\Fixtures\DummyFilter;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Answer;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
+use PHPUnit\Framework\TestCase;
+use Prophecy\Argument;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
+
+class OpenApiFactoryTest extends TestCase
+{
+ private const OPERATION_FORMATS = [
+ 'input_formats' => ['jsonld' => ['application/ld+json']],
+ 'output_formats' => ['jsonld' => ['application/ld+json']],
+ ];
+
+ public function testInvoke(): void
+ {
+ $dummyMetadata = new ResourceMetadata(
+ 'Dummy',
+ 'This is a dummy.',
+ 'http://schema.example.com/Dummy',
+ [
+ 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
+ 'put' => ['method' => 'PUT'] + self::OPERATION_FORMATS,
+ 'delete' => ['method' => 'DELETE'] + self::OPERATION_FORMATS,
+ 'custom' => ['method' => 'HEAD', 'path' => '/foo/{id}', 'openapi_context' => ['description' => 'Custom description']] + self::OPERATION_FORMATS,
+ 'formats' => ['method' => 'PUT', 'path' => '/formatted/{id}', 'output_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']], 'input_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']]],
+ ],
+ [
+ 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
+ 'post' => ['method' => 'POST'] + self::OPERATION_FORMATS,
+ 'filtered' => ['method' => 'GET', 'filters' => ['f1', 'f2', 'f3', 'f4', 'f5'], 'path' => '/filtered'] + self::OPERATION_FORMATS,
+ 'paginated' => ['method' => 'GET', 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => true, 'pagination_items_per_page' => 20, 'pagination_maximum_items_per_page' => 80, 'path' => '/paginated'] + self::OPERATION_FORMATS,
+ ],
+ ['pagination_client_items_per_page' => true]
+ );
+
+ $subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class);
+ $subresourceOperationFactoryProphecy->create(Argument::any())->willReturn([]);
+
+ $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
+ $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
+
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate', 'enum']));
+
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, null, null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$']));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->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));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class), 'This is a \DateTimeInterface object.', true, true, true, true, false, false, null, null, []));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'enum', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is an enum.', true, true, true, true, false, false, null, null, ['openapi_context' => ['type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one']]));
+
+ $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $filters = [
+ 'f1' => new DummyFilter(['name' => [
+ 'property' => 'name',
+ 'type' => 'string',
+ 'required' => true,
+ 'strategy' => 'exact',
+ 'openapi' => ['example' => 'bar', 'deprecated' => true, 'allowEmptyValue' => true, 'allowReserved' => true],
+ ]]),
+ 'f2' => new DummyFilter(['ha' => [
+ 'property' => 'foo',
+ 'type' => 'int',
+ 'required' => false,
+ 'strategy' => 'partial',
+ ]]),
+ 'f3' => new DummyFilter(['toto' => [
+ 'property' => 'name',
+ 'type' => 'array',
+ 'is_collection' => true,
+ 'required' => true,
+ 'strategy' => 'exact',
+ ]]),
+ 'f4' => new DummyFilter(['order[name]' => [
+ 'property' => 'name',
+ 'type' => 'string',
+ 'required' => false,
+ 'schema' => [
+ 'type' => 'string',
+ 'enum' => ['asc', 'desc'],
+ ],
+ ]]),
+ ];
+
+ foreach ($filters as $filterId => $filter) {
+ $filterLocatorProphecy->has($filterId)->willReturn(true)->shouldBeCalled();
+ $filterLocatorProphecy->get($filterId)->willReturn($filter)->shouldBeCalled();
+ }
+
+ $filterLocatorProphecy->has('f5')->willReturn(false)->shouldBeCalled();
+
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+ $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
+
+ $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
+
+ $typeFactory = new TypeFactory();
+ $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
+ $typeFactory->setSchemaFactory($schemaFactory);
+
+ $factory = new OpenApiFactory(
+ $resourceNameCollectionFactoryProphecy->reveal(),
+ $resourceMetadataFactory,
+ $propertyNameCollectionFactory,
+ $propertyMetadataFactory,
+ $schemaFactory,
+ $typeFactory,
+ $operationPathResolver,
+ $filterLocatorProphecy->reveal(),
+ $subresourceOperationFactoryProphecy->reveal(),
+ [],
+ new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
+ 'header' => [
+ 'type' => 'header',
+ 'name' => 'Authorization',
+ ],
+ 'query' => [
+ 'type' => 'query',
+ 'name' => 'key',
+ ],
+ ]),
+ new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination')
+ );
+
+ $dummySchema = new Schema('openapi');
+ // $dummySchema = new Model\Schema(false, null, false, false, null, ['url' => 'http://schema.example.com/Dummy']);
+ $dummySchema->setDefinitions(new \ArrayObject([
+ 'type' => 'object',
+ 'description' => 'This is a dummy.',
+ 'properties' => [
+ 'id' => new \ArrayObject([
+ 'type' => 'integer',
+ 'description' => 'This is an id.',
+ 'readOnly' => true,
+ 'minLength' => 3,
+ 'maxLength' => 20,
+ 'pattern' => '^dummyPattern$',
+ ]),
+ 'name' => new \ArrayObject([
+ 'type' => 'string',
+ 'description' => 'This is a name.',
+ ]),
+ 'description' => new \ArrayObject([
+ 'type' => 'string',
+ 'description' => 'This is an initializable but not writable property.',
+ ]),
+ 'dummy_date' => new \ArrayObject([
+ 'type' => 'string',
+ 'description' => 'This is a \DateTimeInterface object.',
+ 'format' => 'date-time',
+ 'nullable' => true,
+ ]),
+ 'enum' => new \ArrayObject([
+ 'type' => 'string',
+ 'enum' => ['one', 'two'],
+ 'example' => 'one',
+ 'description' => 'This is an enum.',
+ ]),
+ ],
+ 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'],
+ ]));
+
+ $openApi = $factory(['base_url' => '/app_dev.php/']);
+
+ $this->assertInstanceOf(OpenApi::class, $openApi);
+ $this->assertEquals($openApi->getInfo(), new Model\Info('Test API', '1.2.3', 'This is a test API.'));
+ $this->assertEquals($openApi->getServers(), [new Model\Server('/app_dev.php/')]);
+
+ $components = $openApi->getComponents();
+ $this->assertInstanceOf(Model\Components::class, $components);
+
+ $this->assertEquals($components->getSchemas(), new \ArrayObject(['Dummy' => $dummySchema->getDefinitions()]));
+
+ $this->assertEquals($components->getSecuritySchemes(), new \ArrayObject([
+ 'oauth' => new Model\SecurityScheme('oauth2', 'OAuth 2.0 authorization code Grant', null, null, 'oauth2', null, new Model\OAuthFlows(null, null, null, new Model\OAuthFlow('/oauth/v2/auth', '/oauth/v2/token', '/oauth/v2/refresh', new \ArrayObject(['scope param'])))),
+ 'header' => new Model\SecurityScheme('apiKey', 'Value for the Authorization header parameter.', 'Authorization', 'header', 'bearer'),
+ 'query' => new Model\SecurityScheme('apiKey', 'Value for the key query parameter.', 'key', 'query', 'bearer'),
+ ]));
+
+ $paths = $openApi->getPaths();
+ $dummiesPath = $paths->getPath('/dummies');
+ $this->assertNotNull($dummiesPath);
+ foreach (['Put', 'Head', 'Trace', 'Delete', 'Options', 'Patch'] as $method) {
+ $this->assertNull($dummiesPath->{'get'.$method}());
+ }
+
+ $this->assertEquals($dummiesPath->getGet(), new Model\Operation(
+ 'getDummyCollection',
+ ['Dummy'],
+ [
+ '200' => new Model\Response('Dummy collection', new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(new \ArrayObject([
+ 'type' => 'array',
+ 'items' => ['$ref' => '#/components/schemas/Dummy'],
+ ]))),
+ ])),
+ ],
+ '',
+ 'Retrieves the collection of Dummy resources.',
+ null,
+ [
+ new Model\Parameter('page', 'query', 'The collection page number', false, false, true, [
+ 'type' => 'integer',
+ 'default' => 1,
+ ]),
+ new Model\Parameter('itemsPerPage', 'query', 'The number of items per page', false, false, true, [
+ 'type' => 'integer',
+ 'default' => 30,
+ 'minimum' => 0,
+ ]),
+ new Model\Parameter('pagination', 'query', 'Enable or disable pagination', false, false, true, [
+ 'type' => 'boolean',
+ ]),
+ ]
+ ));
+
+ $this->assertEquals($dummiesPath->getPost(), new Model\Operation(
+ 'postDummyCollection',
+ ['Dummy'],
+ [
+ '201' => new Model\Response(
+ 'Dummy resource created',
+ new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Dummy']))),
+ ]),
+ null,
+ new \ArrayObject(['GetDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), [], 'The `id` value returned in the response can be used as the `id` parameter in `GET /dummies/{id}`.')])
+ ),
+ '400' => new Model\Response('Invalid input'),
+ ],
+ '',
+ 'Creates a Dummy resource.',
+ null,
+ [],
+ new Model\RequestBody(
+ 'The new Dummy resource',
+ new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Dummy']))),
+ ]),
+ true
+ )
+ ));
+
+ $dummyPath = $paths->getPath('/dummies/{id}');
+ $this->assertNotNull($dummyPath);
+ foreach (['Post', 'Head', 'Trace', 'Options', 'Patch'] as $method) {
+ $this->assertNull($dummyPath->{'get'.$method}());
+ }
+
+ $this->assertEquals($dummyPath->getGet(), new Model\Operation(
+ 'getDummyItem',
+ ['Dummy'],
+ [
+ '200' => new Model\Response(
+ 'Dummy resource',
+ new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Dummy']))),
+ ])
+ ),
+ '404' => new Model\Response('Resource not found'),
+ ],
+ '',
+ 'Retrieves a Dummy resource.',
+ null,
+ [new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])]
+ ));
+
+ $this->assertEquals($dummyPath->getPut(), new Model\Operation(
+ 'putDummyItem',
+ ['Dummy'],
+ [
+ '200' => new Model\Response(
+ 'Dummy resource updated',
+ new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])),
+ ]),
+ null,
+ new \ArrayObject(['GetDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), [], 'The `id` value returned in the response can be used as the `id` parameter in `GET /dummies/{id}`.')])
+ ),
+ '400' => new Model\Response('Invalid input'),
+ '404' => new Model\Response('Resource not found'),
+ ],
+ '',
+ 'Replaces the Dummy resource.',
+ null,
+ [new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])],
+ new Model\RequestBody(
+ 'The updated Dummy resource',
+ new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])),
+ ]),
+ true
+ )
+ ));
+
+ $this->assertEquals($dummyPath->getDelete(), new Model\Operation(
+ 'deleteDummyItem',
+ ['Dummy'],
+ [
+ '204' => new Model\Response('Dummy resource deleted'),
+ '404' => new Model\Response('Resource not found'),
+ ],
+ '',
+ 'Removes the Dummy resource.',
+ null,
+ [new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])]
+ ));
+
+ $customPath = $paths->getPath('/foo/{id}');
+ $this->assertEquals($customPath->getHead(), new Model\Operation(
+ 'customDummyItem',
+ ['Dummy'],
+ [
+ '404' => new Model\Response('Resource not found'),
+ ],
+ '',
+ 'Custom description',
+ null,
+ [new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])]
+ ));
+
+ $formattedPath = $paths->getPath('/formatted/{id}');
+ $this->assertEquals($formattedPath->getPut(), new Model\Operation(
+ 'formatsDummyItem',
+ ['Dummy'],
+ [
+ '200' => new Model\Response(
+ 'Dummy resource updated',
+ new \ArrayObject([
+ 'application/json' => new Model\MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])),
+ 'text/csv' => new Model\MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])),
+ ]),
+ null,
+ new \ArrayObject(['GetDummyItem' => new Model\Link('getDummyItem', new \ArrayObject(['id' => '$response.body#/id']), [], 'The `id` value returned in the response can be used as the `id` parameter in `GET /dummies/{id}`.')])
+ ),
+ '400' => new Model\Response('Invalid input'),
+ '404' => new Model\Response('Resource not found'),
+ ],
+ '',
+ 'Replaces the Dummy resource.',
+ null,
+ [new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])],
+ new Model\RequestBody(
+ 'The updated Dummy resource',
+ new \ArrayObject([
+ 'application/json' => new Model\MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])),
+ 'text/csv' => new Model\MediaType(new \ArrayObject(['$ref' => '#/components/schemas/Dummy'])),
+ ]),
+ true
+ )
+ ));
+
+ $filteredPath = $paths->getPath('/filtered');
+ $this->assertEquals($filteredPath->getGet(), new Model\Operation(
+ 'filteredDummyCollection',
+ ['Dummy'],
+ [
+ '200' => new Model\Response('Dummy collection', new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject([
+ 'type' => 'array',
+ 'items' => ['$ref' => '#/components/schemas/Dummy'],
+ ])),
+ ])),
+ ],
+ '',
+ 'Retrieves the collection of Dummy resources.',
+ null,
+ [
+ new Model\Parameter('page', 'query', 'The collection page number', false, false, true, [
+ 'type' => 'integer',
+ 'default' => 1,
+ ]),
+ new Model\Parameter('itemsPerPage', 'query', 'The number of items per page', false, false, true, [
+ 'type' => 'integer',
+ 'default' => 30,
+ 'minimum' => 0,
+ ]),
+ new Model\Parameter('pagination', 'query', 'Enable or disable pagination', false, false, true, [
+ 'type' => 'boolean',
+ ]),
+ new Model\Parameter('name', 'query', '', true, true, true, [
+ 'type' => 'string',
+ ], 'form', false, true, 'bar'),
+ new Model\Parameter('ha', 'query', '', false, false, true, [
+ 'type' => 'integer',
+ ]),
+ new Model\Parameter('toto', 'query', '', true, false, true, [
+ 'type' => 'array',
+ 'items' => ['type' => 'string'],
+ ], 'deepObject', true),
+ new Model\Parameter('order[name]', 'query', '', false, false, true, [
+ 'type' => 'string',
+ ]),
+ ]
+ ));
+
+ $paginatedPath = $paths->getPath('/paginated');
+ $this->assertEquals($paginatedPath->getGet(), new Model\Operation(
+ 'paginatedDummyCollection',
+ ['Dummy'],
+ [
+ '200' => new Model\Response('Dummy collection', new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject([
+ 'type' => 'array',
+ 'items' => ['$ref' => '#/components/schemas/Dummy'],
+ ])),
+ ])),
+ ],
+ '',
+ 'Retrieves the collection of Dummy resources.',
+ null,
+ [
+ new Model\Parameter('page', 'query', 'The collection page number', false, false, true, [
+ 'type' => 'integer',
+ 'default' => 1,
+ ]),
+ new Model\Parameter('itemsPerPage', 'query', 'The number of items per page', false, false, true, [
+ 'type' => 'integer',
+ 'default' => 20,
+ 'minimum' => 0,
+ 'maximum' => 80,
+ ]),
+ new Model\Parameter('pagination', 'query', 'Enable or disable pagination', false, false, true, [
+ 'type' => 'boolean',
+ ]),
+ ]
+ ));
+ }
+
+ public function testOverrideDocumentation()
+ {
+ $defaultContext = ['base_url' => '/app_dev.php/'];
+
+ $dummyMetadata = new ResourceMetadata(
+ 'Dummy',
+ 'This is a dummy.',
+ 'http://schema.example.com/Dummy',
+ [
+ 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
+ 'put' => ['method' => 'PUT'] + self::OPERATION_FORMATS,
+ 'delete' => ['method' => 'DELETE'] + self::OPERATION_FORMATS,
+ ],
+ [
+ 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
+ 'post' => ['method' => 'POST'] + self::OPERATION_FORMATS,
+ ],
+ []
+ );
+
+ $subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class);
+ $subresourceOperationFactoryProphecy->create(Argument::any())->willReturn([]);
+
+ $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
+ $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
+
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate']));
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, null, null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$']));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->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));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class), 'This is a \DateTimeInterface object.', true, true, true, true, false, false, null, null, []));
+
+ $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+ $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
+ $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
+
+ $typeFactory = new TypeFactory();
+ $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
+ $typeFactory->setSchemaFactory($schemaFactory);
+
+ $factory = new OpenApiFactory(
+ $resourceNameCollectionFactoryProphecy->reveal(),
+ $resourceMetadataFactory,
+ $propertyNameCollectionFactory,
+ $propertyMetadataFactory,
+ $schemaFactory,
+ $typeFactory,
+ $operationPathResolver,
+ $filterLocatorProphecy->reveal(),
+ $subresourceOperationFactoryProphecy->reveal(),
+ [],
+ new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
+ 'header' => [
+ 'type' => 'header',
+ 'name' => 'Authorization',
+ ],
+ 'query' => [
+ 'type' => 'query',
+ 'name' => 'key',
+ ],
+ ]),
+ new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination')
+ );
+
+ $openApi = $factory(['base_url' => '/app_dev.php/']);
+
+ $pathItem = $openApi->getPaths()->getPath('/dummies/{id}');
+ $operation = $pathItem->getGet();
+
+ $openApi->getPaths()->addPath('/dummies/{id}', $pathItem->withGet(
+ $operation->withParameters(array_merge(
+ $operation->getParameters(),
+ [new Model\Parameter('fields', 'query', 'Fields to remove of the output')]
+ ))
+ ));
+
+ $openApi = $openApi->withInfo((new Model\Info('New Title', 'v2', 'Description of my custom API'))->withExtensionProperty('info-key', 'Info value'));
+ $openApi = $openApi->withExtensionProperty('key', 'Custom x-key value');
+ $openApi = $openApi->withExtensionProperty('x-value', 'Custom x-value value');
+
+ $this->assertEquals($openApi->getInfo()->getExtensionProperties(), ['x-info-key' => 'Info value']);
+ $this->assertEquals($openApi->getExtensionProperties(), ['x-key' => 'Custom x-key value', 'x-value' => 'Custom x-value value']);
+ }
+
+ public function testSubresourceDocumentation()
+ {
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(Question::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['answer']));
+ $propertyNameCollectionFactoryProphecy->create(Answer::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['content']));
+
+ $questionMetadata = new ResourceMetadata(
+ 'Question',
+ 'This is a question.',
+ 'http://schema.example.com/Question',
+ ['get' => ['method' => 'GET', 'input_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']], 'output_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']]]]
+ );
+ $answerMetadata = new ResourceMetadata(
+ 'Answer',
+ 'This is an answer.',
+ 'http://schema.example.com/Answer',
+ [],
+ ['get' => ['method' => 'GET'] + self::OPERATION_FORMATS],
+ [],
+ ['get' => ['method' => 'GET', 'input_formats' => ['xml' => ['text/xml']], 'output_formats' => ['xml' => ['text/xml']]]]
+ );
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Question::class)->shouldBeCalled()->willReturn($questionMetadata);
+ $resourceMetadataFactoryProphecy->create(Answer::class)->shouldBeCalled()->willReturn($answerMetadata);
+
+ $subresourceMetadata = new SubresourceMetadata(Answer::class, false);
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Question::class, 'answer', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, false, Question::class, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Answer::class)), 'This is a name.', true, true, true, true, false, false, null, null, [], $subresourceMetadata));
+
+ $propertyMetadataFactoryProphecy->create(Answer::class, 'content', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, false, Question::class, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Answer::class)), 'This is a name.', true, true, true, true, false, false, null, null, []));
+
+ $routeCollection = new RouteCollection();
+ $routeCollection->add('api_answers_get_collection', new Route('/api/answers.{_format}'));
+ $routeCollection->add('api_questions_answer_get_subresource', new Route('/api/questions/{id}/answer.{_format}'));
+ $routeCollection->add('api_questions_get_item', new Route('/api/questions/{id}.{_format}'));
+
+ $routerProphecy = $this->prophesize(RouterInterface::class);
+ $routerProphecy->getRouteCollection()->shouldBeCalled()->willReturn($routeCollection);
+
+ $operationPathResolver = new RouterOperationPathResolver($routerProphecy->reveal(), new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator())));
+
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+ $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
+ $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
+
+ $subresourceOperationFactory = new SubresourceOperationFactory($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new UnderscorePathSegmentNameGenerator());
+
+ $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
+ $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Question::class, Answer::class]));
+
+ $typeFactory = new TypeFactory();
+ $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
+ $typeFactory->setSchemaFactory($schemaFactory);
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+
+ $factory = new OpenApiFactory(
+ $resourceNameCollectionFactoryProphecy->reveal(),
+ $resourceMetadataFactory,
+ $propertyNameCollectionFactory,
+ $propertyMetadataFactory,
+ $schemaFactory,
+ $typeFactory,
+ $operationPathResolver,
+ $filterLocatorProphecy->reveal(),
+ $subresourceOperationFactory,
+ ['jsonld' => ['application/ld+json']],
+ new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
+ 'header' => [
+ 'type' => 'header',
+ 'name' => 'Authorization',
+ ],
+ 'query' => [
+ 'type' => 'query',
+ 'name' => 'key',
+ ],
+ ]),
+ new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination')
+ );
+
+ $openApi = $factory(['base_url', '/app_dev.php/']);
+
+ $paths = $openApi->getPaths();
+ $pathItem = $paths->getPath('/questions/{id}/answer.{_format}');
+
+ $this->assertEquals($pathItem->getGet(), new Model\Operation(
+ 'api_questions_answer_get_subresourceQuestionSubresource',
+ ['Answer', 'Question'],
+ [
+ '200' => new Model\Response(
+ 'Question resource',
+ new \ArrayObject([
+ 'application/ld+json' => new Model\MediaType(new \ArrayObject(new \ArrayObject(['$ref' => '#/components/schemas/Question']))),
+ ])
+ ),
+ ],
+ '',
+ 'Retrieves a Question resource.',
+ null,
+ [new Model\Parameter('id', 'path', 'Question identifier', true, false, false, ['type' => 'string'])]
+ ));
+ }
+}
diff --git a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php
new file mode 100644
index 00000000000..30444db0e4c
--- /dev/null
+++ b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php
@@ -0,0 +1,158 @@
+
+ *
+ * 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\OpenApi\Serializer;
+
+use ApiPlatform\Core\DataProvider\PaginationOptions;
+use ApiPlatform\Core\JsonSchema\SchemaFactory;
+use ApiPlatform\Core\JsonSchema\TypeFactory;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactory;
+use ApiPlatform\Core\OpenApi\Model;
+use ApiPlatform\Core\OpenApi\Options;
+use ApiPlatform\Core\OpenApi\Serializer\OpenApiNormalizer;
+use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
+use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator;
+use ApiPlatform\Core\PathResolver\CustomOperationPathResolver;
+use ApiPlatform\Core\PathResolver\OperationPathResolver;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use PHPUnit\Framework\TestCase;
+use Prophecy\Argument;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
+use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
+use Symfony\Component\Serializer\Serializer;
+
+class OpenApiNormalizerTest extends TestCase
+{
+ private const OPERATION_FORMATS = [
+ 'input_formats' => ['jsonld' => ['application/ld+json']],
+ 'output_formats' => ['jsonld' => ['application/ld+json']],
+ ];
+
+ public function testNormalize()
+ {
+ $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
+ $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class]));
+ $defaultContext = ['base_url' => '/app_dev.php/'];
+ $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
+ $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate']));
+
+ $dummyMetadata = new ResourceMetadata(
+ 'Dummy',
+ 'This is a dummy.',
+ 'http://schema.example.com/Dummy',
+ [
+ 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
+ 'put' => ['method' => 'PUT'] + self::OPERATION_FORMATS,
+ 'delete' => ['method' => 'DELETE'] + self::OPERATION_FORMATS,
+ ],
+ [
+ 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
+ 'post' => ['method' => 'POST'] + self::OPERATION_FORMATS,
+ ],
+ []
+ );
+
+ $subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class);
+ $subresourceOperationFactoryProphecy->create(Argument::any(), Argument::any(), Argument::any())->willReturn([]);
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
+
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, null, null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$']));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->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));
+ $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class), 'This is a \DateTimeInterface object.', true, true, true, true, false, false, null, null, []));
+
+ $operationPathResolver = new CustomOperationPathResolver(new OperationPathResolver(new UnderscorePathSegmentNameGenerator()));
+ $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
+ $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
+ $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
+ $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
+
+ $typeFactory = new TypeFactory();
+ $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
+ $typeFactory->setSchemaFactory($schemaFactory);
+
+ $factory = new OpenApiFactory(
+ $resourceNameCollectionFactoryProphecy->reveal(),
+ $resourceMetadataFactory,
+ $propertyNameCollectionFactory,
+ $propertyMetadataFactory,
+ $schemaFactory,
+ $typeFactory,
+ $operationPathResolver,
+ $filterLocatorProphecy->reveal(),
+ $subresourceOperationFactoryProphecy->reveal(),
+ [],
+ new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
+ 'header' => [
+ 'type' => 'header',
+ 'name' => 'Authorization',
+ ],
+ 'query' => [
+ 'type' => 'query',
+ 'name' => 'key',
+ ],
+ ]),
+ new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination')
+ );
+
+ $openApi = $factory(['base_url' => '/app_dev.php/']);
+
+ $pathItem = $openApi->getPaths()->getPath('/dummies/{id}');
+ $operation = $pathItem->getGet();
+
+ $openApi->getPaths()->addPath('/dummies/{id}', $pathItem->withGet(
+ $operation->withParameters(array_merge(
+ $operation->getParameters(),
+ [new Model\Parameter('fields', 'query', 'Fields to remove of the output')]
+ ))
+ ));
+
+ $openApi = $openApi->withInfo((new Model\Info('New Title', 'v2', 'Description of my custom API'))->withExtensionProperty('info-key', 'Info value'));
+ $openApi = $openApi->withExtensionProperty('key', 'Custom x-key value');
+ $openApi = $openApi->withExtensionProperty('x-value', 'Custom x-value value');
+
+ $encoders = [new JsonEncoder()];
+ $normalizers = [new ObjectNormalizer()];
+
+ $serializer = new Serializer($normalizers, $encoders);
+ $normalizers[0]->setSerializer($serializer);
+
+ $normalizer = new OpenApiNormalizer($normalizers[0]);
+
+ $openApiAsArray = $normalizer->normalize($openApi);
+
+ // Just testing normalization specifics
+ $this->assertEquals($openApiAsArray['x-key'], 'Custom x-key value');
+ $this->assertEquals($openApiAsArray['x-value'], 'Custom x-value value');
+ $this->assertEquals($openApiAsArray['info']['x-info-key'], 'Info value');
+ $this->assertArrayNotHasKey('extensionProperties', $openApiAsArray);
+ // this key is null, should not be in the output
+ $this->assertArrayNotHasKey('termsOfService', $openApiAsArray['info']);
+ $this->assertArrayNotHasKey('paths', $openApiAsArray['paths']);
+ $this->assertArrayHasKey('/dummies/{id}', $openApiAsArray['paths']);
+ }
+}