Skip to content

Commit 7ac5857

Browse files
jockosdunglas
authored andcommitted
add command to generate json schema (#2996)
* add command to generate json schema * fix review
1 parent 1c19bf5 commit 7ac5857

File tree

5 files changed

+212
-0
lines changed

5 files changed

+212
-0
lines changed

src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
2323
</service>
2424
<service id="ApiPlatform\Core\JsonSchema\SchemaFactoryInterface" alias="api_platform.json_schema.schema_factory" />
25+
26+
<service id="api_platform.json_schema.json_schema_generate_command" class="ApiPlatform\Core\JsonSchema\Command\JsonSchemaGenerateCommand">
27+
<argument type="service" id="api_platform.json_schema.schema_factory"/>
28+
<argument>%api_platform.formats%</argument>
29+
<tag name="console.command" />
30+
</service>
2531
</services>
2632

2733
</container>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\JsonSchema\Command;
15+
16+
use ApiPlatform\Core\Api\OperationType;
17+
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Exception\InvalidOptionException;
20+
use Symfony\Component\Console\Input\InputArgument;
21+
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Input\InputOption;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
use Symfony\Component\Console\Style\SymfonyStyle;
25+
26+
/**
27+
* Generates a resource JSON Schema.
28+
*
29+
* @author Jacques Lefebvre <[email protected]>
30+
*/
31+
final class JsonSchemaGenerateCommand extends Command
32+
{
33+
private $schemaFactory;
34+
private $formats;
35+
36+
public function __construct(SchemaFactoryInterface $schemaFactory, array $formats)
37+
{
38+
$this->schemaFactory = $schemaFactory;
39+
$this->formats = array_keys($formats);
40+
41+
parent::__construct();
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function configure()
48+
{
49+
$this
50+
->setName('api:json-schema:generate')
51+
->setDescription('Generates the JSON Schema for a resource operation.')
52+
->addArgument('resource', InputArgument::REQUIRED, 'The Fully Qualified Class Name (FQCN) of the resource')
53+
->addOption('itemOperation', null, InputOption::VALUE_REQUIRED, 'The item operation')
54+
->addOption('collectionOperation', null, InputOption::VALUE_REQUIRED, 'The collection operation')
55+
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The response format', (string) $this->formats[0])
56+
->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of schema to generate (input or output)', 'input');
57+
}
58+
59+
/**
60+
* {@inheritdoc}
61+
*/
62+
protected function execute(InputInterface $input, OutputInterface $output)
63+
{
64+
$io = new SymfonyStyle($input, $output);
65+
66+
/** @var string $resource */
67+
$resource = $input->getArgument('resource');
68+
/** @var ?string $itemOperation */
69+
$itemOperation = $input->getOption('itemOperation');
70+
/** @var ?string $collectionOperation */
71+
$collectionOperation = $input->getOption('collectionOperation');
72+
/** @var string $format */
73+
$format = $input->getOption('format');
74+
/** @var string $outputType */
75+
$outputType = $input->getOption('type');
76+
77+
if (!\in_array($outputType, ['input', 'output'], true)) {
78+
$io->error('You can only use "input" or "output" for the "type" option');
79+
80+
return 1;
81+
}
82+
83+
if (!\in_array($format, $this->formats, true)) {
84+
throw new InvalidOptionException(sprintf('The response format "%s" is not supported. Supported formats are : %s.', $format, implode(', ', $this->formats)));
85+
}
86+
87+
/** @var ?string $operationType */
88+
$operationType = null;
89+
/** @var ?string $operationName */
90+
$operationName = null;
91+
92+
if ($itemOperation && $collectionOperation) {
93+
$io->error('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.');
94+
95+
return 1;
96+
}
97+
98+
if (null !== $itemOperation || null !== $collectionOperation) {
99+
$operationType = $itemOperation ? OperationType::ITEM : OperationType::COLLECTION;
100+
$operationName = $itemOperation ?? $collectionOperation;
101+
}
102+
103+
$schema = $this->schemaFactory->buildSchema($resource, $format, 'output' === $outputType, $operationType, $operationName);
104+
105+
if (null !== $operationType && null !== $operationName && !$schema->isDefined()) {
106+
$io->error(sprintf('There is no %ss defined for the operation "%s" of the resource "%s".', $outputType, $operationName, $resource));
107+
108+
return 1;
109+
}
110+
111+
$io->text((string) json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
112+
113+
return 0;
114+
}
115+
}

src/JsonSchema/SchemaFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ public function buildSchema(string $resourceClass, string $format = 'json', bool
7272

7373
$version = $schema->getVersion();
7474
$definitionName = $this->buildDefinitionName($resourceClass, $format, $output, $operationType, $operationName, $serializerContext);
75+
76+
$method = (null !== $operationType && null !== $operationName) ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method') : 'GET';
77+
78+
if (!$output && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
79+
return $schema;
80+
}
81+
7582
if (!isset($schema['$ref']) && !isset($schema['type'])) {
7683
$ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
7784

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
11841184
'api_platform.swagger.normalizer.documentation',
11851185
'api_platform.json_schema.type_factory',
11861186
'api_platform.json_schema.schema_factory',
1187+
'api_platform.json_schema.json_schema_generate_command',
11871188
'api_platform.validator',
11881189
'test.api_platform.client',
11891190
];
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\JsonSchema\Command;
15+
16+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
18+
use Symfony\Bundle\FrameworkBundle\Console\Application;
19+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
20+
use Symfony\Component\Console\Tester\ApplicationTester;
21+
22+
/**
23+
* @author Jacques Lefebvre <[email protected]>
24+
*/
25+
class JsonSchemaGenerateCommandTest extends KernelTestCase
26+
{
27+
/**
28+
* @var ApplicationTester
29+
*/
30+
private $tester;
31+
32+
private $entityClass;
33+
34+
protected function setUp(): void
35+
{
36+
$kernel = self::bootKernel();
37+
38+
$application = new Application(static::$kernel);
39+
$application->setCatchExceptions(true);
40+
$application->setAutoExit(false);
41+
42+
$this->entityClass = 'mongodb' === $kernel->getEnvironment() ? DocumentDummy::class : Dummy::class;
43+
$this->tester = new ApplicationTester($application);
44+
}
45+
46+
public function testExecuteWithoutOption()
47+
{
48+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass]);
49+
50+
$this->assertJson($this->tester->getDisplay());
51+
}
52+
53+
public function testExecuteWithItemOperationGet()
54+
{
55+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--itemOperation' => 'get', '--type' => 'output']);
56+
57+
$this->assertJson($this->tester->getDisplay());
58+
}
59+
60+
public function testExecuteWithCollectionOperationGet()
61+
{
62+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'get', '--type' => 'output']);
63+
64+
$this->assertJson($this->tester->getDisplay());
65+
}
66+
67+
public function testExecuteWithTooManyOptions()
68+
{
69+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'get', '--itemOperation' => 'get', '--type' => 'output']);
70+
71+
$this->assertStringStartsWith('[ERROR] You can only use one of "--itemOperation" and "--collectionOperation"', trim(str_replace(["\r", "\n"], '', $this->tester->getDisplay())));
72+
}
73+
74+
public function testExecuteWithJsonldFormatOption()
75+
{
76+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--collectionOperation' => 'post', '--format' => 'jsonld']);
77+
$result = $this->tester->getDisplay();
78+
79+
$this->assertStringContainsString('@id', $result);
80+
$this->assertStringContainsString('@context', $result);
81+
$this->assertStringContainsString('@type', $result);
82+
}
83+
}

0 commit comments

Comments
 (0)