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); + } +}