Skip to content

Commit e8f26ca

Browse files
authored
[OpenAPI] Extract the JSON Schema builder (#2983)
* [OpenAPI] Extract the JSON Schema builder * Refactor DocumentationNormalizer * Add tests for JsonSchema
1 parent 92f655c commit e8f26ca

File tree

18 files changed

+1042
-450
lines changed

18 files changed

+1042
-450
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"symfony/http-client": "^4.3",
7676
"symfony/mercure-bundle": "*",
7777
"symfony/messenger": "^4.3",
78-
"symfony/phpunit-bridge": "^4.3.1",
78+
"symfony/phpunit-bridge": "^4.3@dev",
7979
"symfony/routing": "^3.4 || ^4.0",
8080
"symfony/security-bundle": "^3.4 || ^4.0",
8181
"symfony/security-core": "^4.3",

features/openapi/docs.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Feature: Documentation support
9595
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 6 elements
9696

9797
# Subcollection - check schema
98-
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.items.$ref" should be equal to "#/components/schemas/RelatedToDummyFriend-fakemanytomany"
98+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.properties.hydra:member.items.$ref" should be equal to "#/components/schemas/RelatedToDummyFriend:jsonld-fakemanytomany"
9999

100100
# Deprecations
101101
And the JSON node "paths./dummies.get.deprecated" should not exist

phpstan.neon.dist

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@ parameters:
5151
- '#Parameter \#1 \$defaultContext of class Symfony\\Component\\Serializer\\Encoder\\Json(De|En)code constructor expects array, (int|true) given\.#'
5252
- '#Parameter \#(2|3) \$(resourceMetadataFactory|pagination) of class ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\PaginationExtension constructor expects (ApiPlatform\\Core\\Metadata\\Resource\\Factory\\ResourceMetadataFactoryInterface\|Symfony\\Component\\HttpFoundation\\RequestStack|ApiPlatform\\Core\\DataProvider\\Pagination\|ApiPlatform\\Core\\Metadata\\Resource\\Factory\\ResourceMetadataFactoryInterface), stdClass given\.#'
5353
- '#Parameter \#[0-9] \$filterLocator of class ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\FilterExtension constructor expects ApiPlatform\\Core\\Api\\FilterCollection|Psr\\Container\\ContainerInterface(\|null)?, ArrayObject given\.#'
54-
-
55-
message: '#Parameter \#9 \$nameConverter of class ApiPlatform\\Core\\Swagger\\Serializer\\DocumentationNormalizer constructor expects Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface\|null, object given\.#'
56-
path: %currentWorkingDirectory%/tests/Swagger/Serializer/DocumentationNormalizer*Test.php
5754
-
5855
message: '#Parameter \#1 \$resource of method ApiPlatform\\Core\\Metadata\\Extractor\\XmlExtractor::getAttributes\(\) expects SimpleXMLElement, object given\.#'
5956
path: %currentWorkingDirectory%/src/Metadata/Extractor/XmlExtractor.php

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array
311311
return;
312312
}
313313

314+
$loader->load('json_schema.xml');
314315
$loader->load('swagger.xml');
315316

316317
if ($config['enable_swagger_ui'] || $config['enable_re_doc']) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@
7676
<argument type="service" id="api_platform.filter_locator" />
7777
</service>
7878

79+
<!-- JSON Schema -->
80+
81+
<service id="api_platform.hydra.json_schema.schema_factory" class="ApiPlatform\Core\Hydra\JsonSchema\SchemaFactory" decorates="api_platform.json_schema.schema_factory">
82+
<argument type="service" id="api_platform.hydra.json_schema.schema_factory.inner" />
83+
</service>
84+
7985
</services>
8086

8187
</container>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<defaults public="false" />
9+
10+
<service id="api_platform.json_schema.type_factory" class="ApiPlatform\Core\JsonSchema\TypeFactory">
11+
<call method="setSchemaFactory">
12+
<argument type="service" id="api_platform.json_schema.schema_factory"/>
13+
</call>
14+
</service>
15+
<service id="ApiPlatform\Core\JsonSchema\TypeFactoryInterface" alias="api_platform.json_schema.type_factory" />
16+
17+
<service id="api_platform.json_schema.schema_factory" class="ApiPlatform\Core\JsonSchema\SchemaFactory">
18+
<argument type="service" id="api_platform.json_schema.type_factory"></argument>
19+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
20+
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
21+
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
22+
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
23+
</service>
24+
<service id="ApiPlatform\Core\JsonSchema\SchemaFactoryInterface" alias="api_platform.json_schema.schema_factory" />
25+
</services>
26+
27+
</container>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
1111
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
1212
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
13-
<argument type="service" id="api_platform.resource_class_resolver" />
14-
<argument>null</argument>
13+
<argument type="service" id="api_platform.json_schema.schema_factory" />
14+
<argument type="service" id="api_platform.json_schema.type_factory" />
1515
<argument type="service" id="api_platform.operation_path_resolver" />
1616
<argument>null</argument>
1717
<argument type="service" id="api_platform.filter_locator" />
18-
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
18+
<argument>null</argument>
1919
<argument>%api_platform.oauth.enabled%</argument>
2020
<argument>%api_platform.oauth.type%</argument>
2121
<argument>%api_platform.oauth.flow%</argument>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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\Hydra\JsonSchema;
15+
16+
use ApiPlatform\Core\JsonSchema\Schema;
17+
use ApiPlatform\Core\JsonSchema\SchemaFactory as BaseSchemaFactory;
18+
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
19+
20+
/**
21+
* Generates the JSON Schema corresponding to a Hydra document.
22+
*
23+
* @experimental
24+
*
25+
* @author Kévin Dunglas <[email protected]>
26+
*/
27+
final class SchemaFactory implements SchemaFactoryInterface
28+
{
29+
private const BASE_PROP = [
30+
'readOnly' => true,
31+
'type' => 'string',
32+
];
33+
private const BASE_PROPS = [
34+
'@context' => self::BASE_PROP,
35+
'@id' => self::BASE_PROP,
36+
'@type' => self::BASE_PROP,
37+
];
38+
39+
private $schemaFactory;
40+
41+
public function __construct(BaseSchemaFactory $schemaFactory)
42+
{
43+
$this->schemaFactory = $schemaFactory;
44+
$schemaFactory->addDistinctFormat('jsonld');
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function buildSchema(string $resourceClass, string $format = 'jsonld', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
51+
{
52+
$schema = $this->schemaFactory->buildSchema($resourceClass, $format, $output, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
53+
if ('jsonld' !== $format) {
54+
return $schema;
55+
}
56+
57+
$definitions = $schema->getDefinitions();
58+
if ($key = $schema->getRootDefinitionKey()) {
59+
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
60+
61+
return $schema;
62+
}
63+
64+
if (($schema['type'] ?? '') === 'array') {
65+
// hydra:collection
66+
$items = $schema['items'];
67+
unset($schema['items']);
68+
69+
$schema['type'] = 'object';
70+
$schema['properties'] = [
71+
'hydra:member' => [
72+
'type' => 'array',
73+
'items' => $items,
74+
],
75+
'hydra:totalItems' => [
76+
'type' => 'integer',
77+
'minimum' => 1,
78+
],
79+
'hydra:view' => [
80+
'type' => 'object',
81+
'properties' => [
82+
'@id' => ['type' => 'string'],
83+
'@type' => ['type' => 'string'],
84+
'hydra:first' => [
85+
'type' => 'integer',
86+
'minimum' => 1,
87+
],
88+
'hydra:last' => [
89+
'type' => 'integer',
90+
'minimum' => 1,
91+
],
92+
'hydra:next' => [
93+
'type' => 'integer',
94+
'minimum' => 1,
95+
],
96+
],
97+
],
98+
'hydra:search' => [
99+
'type' => 'object',
100+
'properties' => [
101+
'@type' => ['type' => 'string'],
102+
'hydra:template' => ['type' => 'string'],
103+
'hydra:variableRepresentation' => ['type' => 'string'],
104+
'hydra:mapping' => [
105+
'type' => 'array',
106+
'items' => [
107+
'type' => 'object',
108+
'properties' => [
109+
'@type' => ['type' => 'string'],
110+
'variable' => ['type' => 'string'],
111+
'property' => ['type' => 'string'],
112+
'required' => ['type' => 'boolean'],
113+
],
114+
],
115+
],
116+
],
117+
],
118+
];
119+
120+
return $schema;
121+
}
122+
123+
return $schema;
124+
}
125+
}

src/JsonSchema/Schema.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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;
15+
16+
/**
17+
* Represents a JSON Schema document.
18+
*
19+
* Both the standard version and the OpenAPI flavors (v2 and v3) are supported.
20+
*
21+
* @see https://json-schema.org/latest/json-schema-core.html
22+
* @see https://github.com/OAI/OpenAPI-Specification
23+
*
24+
* @experimental
25+
*
26+
* @author Kévin Dunglas <[email protected]>
27+
*/
28+
final class Schema extends \ArrayObject
29+
{
30+
public const VERSION_JSON_SCHEMA = 'json-schema';
31+
public const VERSION_SWAGGER = 'swagger';
32+
public const VERSION_OPENAPI = 'openapi';
33+
34+
private $version;
35+
36+
public function __construct(string $version = self::VERSION_JSON_SCHEMA)
37+
{
38+
$this->version = $version;
39+
40+
parent::__construct(self::VERSION_JSON_SCHEMA === $this->version ? ['$schema' => 'http://json-schema.org/draft-07/schema#'] : []);
41+
}
42+
43+
/**
44+
* The flavor used for this document: JSON Schema, OpenAPI v2 or OpenAPI v3.
45+
*/
46+
public function getVersion(): string
47+
{
48+
return $this->version;
49+
}
50+
51+
/**
52+
* @param bool $includeDefinitions if set to false, definitions will not be included in the resulting array
53+
*/
54+
public function getArrayCopy(bool $includeDefinitions = true): array
55+
{
56+
$schema = parent::getArrayCopy();
57+
58+
if (!$includeDefinitions) {
59+
unset($schema['definitions'], $schema['components']);
60+
}
61+
62+
return $schema;
63+
}
64+
65+
/**
66+
* Retrieves the definitions used by this schema.
67+
*/
68+
public function getDefinitions(): \ArrayObject
69+
{
70+
$definitions = $this['definitions'] ?? $this['components']['schemas'] ?? new \ArrayObject();
71+
$this->setDefinitions($definitions);
72+
73+
return $definitions;
74+
}
75+
76+
/**
77+
* Associates existing definitions to this schema.
78+
*/
79+
public function setDefinitions(\ArrayObject $definitions): void
80+
{
81+
if (self::VERSION_OPENAPI === $this->version) {
82+
$this['components']['schemas'] = $definitions;
83+
84+
return;
85+
}
86+
87+
$this['definitions'] = $definitions;
88+
}
89+
90+
/**
91+
* Returns the name of the root definition, if defined.
92+
*/
93+
public function getRootDefinitionKey(): ?string
94+
{
95+
if (!isset($this['$ref'])) {
96+
return null;
97+
}
98+
99+
// strlen('#/definitions/') = 14
100+
// strlen('#/components/schemas/') = 21
101+
$prefix = self::VERSION_OPENAPI === $this->version ? 21 : 14;
102+
103+
return substr($this['$ref'], $prefix);
104+
}
105+
106+
/**
107+
* Checks if this schema is initialized.
108+
*/
109+
public function isDefined(): bool
110+
{
111+
return isset($this['$ref']) || isset($this['type']);
112+
}
113+
}

0 commit comments

Comments
 (0)