Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
</service>
<service id="ApiPlatform\Core\JsonSchema\SchemaFactoryInterface" alias="api_platform.json_schema.schema_factory" />

<service id="api_platform.json_schema.json_schema_generate_command" class="ApiPlatform\Core\JsonSchema\Command\JsonSchemaGenerateCommand">
<argument type="service" id="api_platform.json_schema.schema_factory"/>
<argument>%api_platform.formats%</argument>
<tag name="console.command" />
</service>
</services>

</container>
115 changes: 115 additions & 0 deletions src/JsonSchema/Command/JsonSchemaGenerateCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* Generates a resource JSON Schema.
*
* @author Jacques Lefebvre <jacques@les-tilleuls.coop>
*/
final class JsonSchemaGenerateCommand extends Command
{
private $schemaFactory;
private $formats;

public function __construct(SchemaFactoryInterface $schemaFactory, array $formats)
{
$this->schemaFactory = $schemaFactory;
$this->formats = array_keys($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_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');
}

/**
* {@inheritdoc}
*/
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');
/** @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');

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) {
$io->error('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.');

return 1;
}

if (null !== $itemOperation || null !== $collectionOperation) {
$operationType = $itemOperation ? OperationType::ITEM : OperationType::COLLECTION;
$operationName = $itemOperation ?? $collectionOperation;
}

$schema = $this->schemaFactory->buildSchema($resource, $format, 'output' === $outputType, $operationType, $operationName);

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((string) json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

return 0;
}
}
7 changes: 7 additions & 0 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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, ['POST', 'PATCH', 'PUT'], true)) {
return $schema;
}

if (!isset($schema['$ref']) && !isset($schema['type'])) {
$ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
Expand Down
83 changes: 83 additions & 0 deletions tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;
use Symfony\Component\Console\Tester\ApplicationTester;

/**
* @author Jacques Lefebvre <jacques@les-tilleuls.coop>
*/
class JsonSchemaGenerateCommandTest extends KernelTestCase
{
/**
* @var ApplicationTester
*/
private $tester;

private $entityClass;

protected function setUp(): void
{
$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' => $this->entityClass]);

$this->assertJson($this->tester->getDisplay());
}

public function testExecuteWithItemOperationGet()
{
$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' => $this->entityClass, '--collectionOperation' => 'get', '--type' => 'output']);

$this->assertJson($this->tester->getDisplay());
}

public function testExecuteWithTooManyOptions()
{
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'get', '--itemOperation' => 'get', '--type' => 'output']);

$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' => $this->entityClass, '--collectionOperation' => 'post', '--format' => 'jsonld']);
$result = $this->tester->getDisplay();

$this->assertStringContainsString('@id', $result);
$this->assertStringContainsString('@context', $result);
$this->assertStringContainsString('@type', $result);
}
}