From 892bd1d69c8481e02da4385c78a0fd1c1d08275c Mon Sep 17 00:00:00 2001 From: Jacques Lefebvre Date: Mon, 19 Aug 2019 15:32:41 +0200 Subject: [PATCH 1/2] add command to generate json schema --- .../Bundle/Resources/config/json_schema.xml | 6 ++ .../Command/JsonSchemaGenerateCommand.php | 90 +++++++++++++++++++ src/JsonSchema/SchemaFactory.php | 8 ++ .../Command/JsonSchemaGenerateCommandTest.php | 70 +++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 src/JsonSchema/Command/JsonSchemaGenerateCommand.php create mode 100644 tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php diff --git a/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml b/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml index d407d4d8eec..bf711ba27c5 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml @@ -22,6 +22,12 @@ + + + + %api_platform.formats% + + diff --git a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php new file mode 100644 index 00000000000..10260483b3f --- /dev/null +++ b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php @@ -0,0 +1,90 @@ + + */ +final class JsonSchemaGenerateCommand extends Command +{ + private $schemaFactory; + + private $formats; + + public function __construct(SchemaFactoryInterface $schemaFactory, array $formats) + { + $this->schemaFactory = $schemaFactory; + $this->formats = $formats; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('api:json-schema:generate') + ->setDescription('Generates the JSON Schema for a resource operation.') + ->addArgument('resource', InputArgument::REQUIRED, 'The Fully Qualified Class Name (FQCN) of the resource') + ->addOption('itemOperation', null, InputOption::VALUE_OPTIONAL, 'The item operation') + ->addOption('collectionOperation', null, InputOption::VALUE_OPTIONAL, 'The collection operation') + ->addOption('format', null, InputOption::VALUE_OPTIONAL, 'The response format', array_key_first($this->formats)) + ->addOption('output', null, InputOption::VALUE_NONE, 'Use this option to get the output version') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $resource = $input->getArgument('resource'); + $itemOperation = $input->getOption('itemOperation'); + $collectionOperation = $input->getOption('collectionOperation'); + $format = $input->getOption('format'); + $outputType = $input->getOption('output'); + + if (!isset($this->formats[$format])) { + throw new InvalidOptionException(\sprintf('The response format "%s" is not supported. Supported formats are : %s.', $format, implode(', ', array_keys($this->formats)))); + } + + $operationType = null; + $operationName = null; + + if ($itemOperation && $collectionOperation) { + throw new InvalidOptionException('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.'); + } + + if (null !== $itemOperation || null !== $collectionOperation) { + $operationType = $itemOperation ? OperationType::ITEM : OperationType::COLLECTION; + $operationName = $itemOperation ?? $collectionOperation; + } + + $schema = $this->schemaFactory->buildSchema($resource, $format, $outputType , $operationType, $operationName); + + if (!$schema->isDefined()) { + $io->error(\sprintf('There is no %s defined for the operation "%s" of the resource "%s".', $outputType ? 'outputs': 'inputs', $operationName, $resource)); + return; + } + + $io->text(\json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } +} diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index b7d890584ae..1fa825e142f 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -20,6 +20,7 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -72,6 +73,13 @@ public function buildSchema(string $resourceClass, string $format = 'json', bool $version = $schema->getVersion(); $definitionName = $this->buildDefinitionName($resourceClass, $format, $output, $operationType, $operationName, $serializerContext); + + $method = (null !== $operationType && null !== $operationName) ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method') : 'GET'; + + if (!$output && !in_array($method, [Request::METHOD_POST, Request::METHOD_PATCH, Request::METHOD_PUT], true)) { + return $schema; + } + if (!isset($schema['$ref']) && !isset($schema['type'])) { $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php new file mode 100644 index 00000000000..2270afd2836 --- /dev/null +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -0,0 +1,70 @@ + + */ +class JsonSchemaGenerateCommandTest extends KernelTestCase +{ + /** + * @var ApplicationTester + */ + private $tester; + + protected function setUp(): void + { + self::bootKernel(); + + $application = new Application(static::$kernel); + $application->setCatchExceptions(true); + $application->setAutoExit(false); + + $this->tester = new ApplicationTester($application); + } + + public function testExecuteWithoutOption() + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class]); + + $this->assertJson($this->tester->getDisplay()); + } + + public function testExecuteWithItemOperationGet() + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--itemOperation' => 'get']); + + $this->assertJson($this->tester->getDisplay()); + } + + public function testExecuteWithCollectionOperationGet() + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--collectionOperation' => 'get']); + + $this->assertJson($this->tester->getDisplay()); + } + + public function testExecuteWithTooManyOptions() + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--collectionOperation' => 'get', '--itemOperation' => 'get']); + + $this->assertStringContainsString('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.', $this->tester->getDisplay()); + } + + public function testExecuteWithJsonldFormatOption() + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--format' => 'jsonld']); + $result = $this->tester->getDisplay(); + + $this->assertStringContainsString('@id', $result); + $this->assertStringContainsString('@context', $result); + $this->assertStringContainsString('@type', $result); + } +} From 6ff741ccc4e5929ed1534636ee71c2c2129d4a4b Mon Sep 17 00:00:00 2001 From: Jacques Lefebvre Date: Tue, 20 Aug 2019 09:46:43 +0200 Subject: [PATCH 2/2] fix review --- .../Command/JsonSchemaGenerateCommand.php | 57 +++++++++++++------ src/JsonSchema/SchemaFactory.php | 3 +- .../ApiPlatformExtensionTest.php | 1 + .../Command/JsonSchemaGenerateCommandTest.php | 27 ++++++--- 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php index 10260483b3f..e930854d186 100644 --- a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php +++ b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php @@ -1,5 +1,14 @@ + * + * 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\JsonSchema\Command; @@ -22,13 +31,12 @@ final class JsonSchemaGenerateCommand extends Command { private $schemaFactory; - private $formats; public function __construct(SchemaFactoryInterface $schemaFactory, array $formats) { $this->schemaFactory = $schemaFactory; - $this->formats = $formats; + $this->formats = array_keys($formats); parent::__construct(); } @@ -42,11 +50,10 @@ protected function configure() ->setName('api:json-schema:generate') ->setDescription('Generates the JSON Schema for a resource operation.') ->addArgument('resource', InputArgument::REQUIRED, 'The Fully Qualified Class Name (FQCN) of the resource') - ->addOption('itemOperation', null, InputOption::VALUE_OPTIONAL, 'The item operation') - ->addOption('collectionOperation', null, InputOption::VALUE_OPTIONAL, 'The collection operation') - ->addOption('format', null, InputOption::VALUE_OPTIONAL, 'The response format', array_key_first($this->formats)) - ->addOption('output', null, InputOption::VALUE_NONE, 'Use this option to get the output version') - ; + ->addOption('itemOperation', null, InputOption::VALUE_REQUIRED, 'The item operation') + ->addOption('collectionOperation', null, InputOption::VALUE_REQUIRED, 'The collection operation') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The response format', (string) $this->formats[0]) + ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of schema to generate (input or output)', 'input'); } /** @@ -56,21 +63,36 @@ protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); + /** @var string $resource */ $resource = $input->getArgument('resource'); + /** @var ?string $itemOperation */ $itemOperation = $input->getOption('itemOperation'); + /** @var ?string $collectionOperation */ $collectionOperation = $input->getOption('collectionOperation'); + /** @var string $format */ $format = $input->getOption('format'); - $outputType = $input->getOption('output'); + /** @var string $outputType */ + $outputType = $input->getOption('type'); + + if (!\in_array($outputType, ['input', 'output'], true)) { + $io->error('You can only use "input" or "output" for the "type" option'); - if (!isset($this->formats[$format])) { - throw new InvalidOptionException(\sprintf('The response format "%s" is not supported. Supported formats are : %s.', $format, implode(', ', array_keys($this->formats)))); + return 1; } + if (!\in_array($format, $this->formats, true)) { + throw new InvalidOptionException(sprintf('The response format "%s" is not supported. Supported formats are : %s.', $format, implode(', ', $this->formats))); + } + + /** @var ?string $operationType */ $operationType = null; + /** @var ?string $operationName */ $operationName = null; if ($itemOperation && $collectionOperation) { - throw new InvalidOptionException('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.'); + $io->error('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.'); + + return 1; } if (null !== $itemOperation || null !== $collectionOperation) { @@ -78,13 +100,16 @@ protected function execute(InputInterface $input, OutputInterface $output) $operationName = $itemOperation ?? $collectionOperation; } - $schema = $this->schemaFactory->buildSchema($resource, $format, $outputType , $operationType, $operationName); + $schema = $this->schemaFactory->buildSchema($resource, $format, 'output' === $outputType, $operationType, $operationName); - if (!$schema->isDefined()) { - $io->error(\sprintf('There is no %s defined for the operation "%s" of the resource "%s".', $outputType ? 'outputs': 'inputs', $operationName, $resource)); - return; + if (null !== $operationType && null !== $operationName && !$schema->isDefined()) { + $io->error(sprintf('There is no %ss defined for the operation "%s" of the resource "%s".', $outputType, $operationName, $resource)); + + return 1; } - $io->text(\json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $io->text((string) json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return 0; } } diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 1fa825e142f..923f0bb145d 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -20,7 +20,6 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -76,7 +75,7 @@ public function buildSchema(string $resourceClass, string $format = 'json', bool $method = (null !== $operationType && null !== $operationName) ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method') : 'GET'; - if (!$output && !in_array($method, [Request::METHOD_POST, Request::METHOD_PATCH, Request::METHOD_PUT], true)) { + if (!$output && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) { return $schema; } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 0f9a3a7ae5d..87d3de7c536 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -1184,6 +1184,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.swagger.normalizer.documentation', 'api_platform.json_schema.type_factory', 'api_platform.json_schema.schema_factory', + 'api_platform.json_schema.json_schema_generate_command', 'api_platform.validator', 'test.api_platform.client', ]; diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index 2270afd2836..f6e74c1bef4 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -1,9 +1,19 @@ + * + * 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\JsonSchema\Command; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -19,48 +29,51 @@ class JsonSchemaGenerateCommandTest extends KernelTestCase */ private $tester; + private $entityClass; + protected function setUp(): void { - self::bootKernel(); + $kernel = self::bootKernel(); $application = new Application(static::$kernel); $application->setCatchExceptions(true); $application->setAutoExit(false); + $this->entityClass = 'mongodb' === $kernel->getEnvironment() ? DocumentDummy::class : Dummy::class; $this->tester = new ApplicationTester($application); } public function testExecuteWithoutOption() { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class]); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass]); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithItemOperationGet() { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--itemOperation' => 'get']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--itemOperation' => 'get', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithCollectionOperationGet() { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--collectionOperation' => 'get']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'get', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithTooManyOptions() { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--collectionOperation' => 'get', '--itemOperation' => 'get']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'get', '--itemOperation' => 'get', '--type' => 'output']); - $this->assertStringContainsString('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.', $this->tester->getDisplay()); + $this->assertStringStartsWith('[ERROR] You can only use one of "--itemOperation" and "--collectionOperation"', trim(str_replace(["\r", "\n"], '', $this->tester->getDisplay()))); } public function testExecuteWithJsonldFormatOption() { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'post', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $this->assertStringContainsString('@id', $result);