Skip to content

Commit 67ee6ce

Browse files
committed
fix(json-schema): share invariable sub-schemas
1 parent b0b3fe4 commit 67ee6ce

31 files changed

+1200
-470
lines changed

features/openapi/docs.feature

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Feature: Documentation support
8080
And the JSON node "paths./api/custom-call/{id}.put" should exist
8181
# Properties
8282
And the "id" property exists for the OpenAPI class "Dummy"
83-
And the "name" property is required for the OpenAPI class "Dummy"
83+
And the "name" property is required for the OpenAPI class "Dummy.jsonld"
8484
And the "genderType" property exists for the OpenAPI class "Person"
8585
And the "genderType" property for the OpenAPI class "Person" should be equal to:
8686
"""

src/Hal/JsonSchema/SchemaFactory.php

+118-52
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313

1414
namespace ApiPlatform\Hal\JsonSchema;
1515

16+
use ApiPlatform\JsonSchema\DefinitionNameFactory;
17+
use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface;
18+
use ApiPlatform\JsonSchema\ResourceMetadataTrait;
1619
use ApiPlatform\JsonSchema\Schema;
1720
use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface;
1821
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
22+
use ApiPlatform\JsonSchema\SchemaUriPrefixTrait;
1923
use ApiPlatform\Metadata\Operation;
24+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2025

2126
/**
2227
* Decorator factory which adds HAL properties to the JSON Schema document.
@@ -26,6 +31,11 @@
2631
*/
2732
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
2833
{
34+
use ResourceMetadataTrait;
35+
use SchemaUriPrefixTrait;
36+
37+
private const COLLECTION_BASE_SCHEMA_NAME = 'HalCollectionBaseSchema';
38+
2939
private const HREF_PROP = [
3040
'href' => [
3141
'type' => 'string',
@@ -44,8 +54,12 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
4454
],
4555
];
4656

47-
public function __construct(private readonly SchemaFactoryInterface $schemaFactory)
57+
public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private ?DefinitionNameFactoryInterface $definitionNameFactory = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null)
4858
{
59+
if (!$definitionNameFactory) {
60+
$this->definitionNameFactory = new DefinitionNameFactory();
61+
}
62+
$this->resourceMetadataFactory = $resourceMetadataFactory;
4963
if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
5064
$this->schemaFactory->setSchemaFactory($this);
5165
}
@@ -56,79 +70,131 @@ public function __construct(private readonly SchemaFactoryInterface $schemaFacto
5670
*/
5771
public function buildSchema(string $className, string $format = 'jsonhal', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
5872
{
59-
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
6073
if ('jsonhal' !== $format) {
61-
return $schema;
74+
return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
75+
}
76+
77+
if (!$this->isResourceClass($className)) {
78+
$operation = null;
79+
$inputOrOutputClass = null;
80+
$serializerContext ??= [];
81+
} else {
82+
$operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
83+
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
84+
$serializerContext ??= $this->getSerializerContext($operation, $type);
6285
}
6386

87+
if (null === $inputOrOutputClass) {
88+
// input or output disabled
89+
return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
90+
}
91+
92+
$schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $serializerContext, $forceCollection);
6493
$definitions = $schema->getDefinitions();
65-
if ($key = $schema->getRootDefinitionKey()) {
66-
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
94+
$definitionName = $this->definitionNameFactory->create($className, $format, $className, $operation, $serializerContext);
95+
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
96+
$collectionKey = $schema->getItemsDefinitionKey();
97+
98+
// Already computed
99+
if (!$collectionKey && isset($definitions[$definitionName])) {
100+
$schema['$ref'] = $prefix.$definitionName;
67101

68102
return $schema;
69103
}
70-
if ($key = $schema->getItemsDefinitionKey()) {
71-
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
104+
105+
$key = $schema->getRootDefinitionKey() ?? $collectionKey;
106+
107+
$definitions[$definitionName] = [
108+
'allOf' => [
109+
['type' => 'object', 'properties' => self::BASE_PROPS],
110+
['$ref' => $prefix.$key],
111+
],
112+
];
113+
114+
if (isset($definitions[$key]['description'])) {
115+
$definitions[$definitionName]['description'] = $definitions[$key]['description'];
116+
}
117+
118+
if (!$collectionKey) {
119+
$schema['$ref'] = $prefix.$definitionName;
120+
121+
return $schema;
72122
}
73123

74124
if (($schema['type'] ?? '') === 'array') {
75-
$items = $schema['items'];
76-
unset($schema['items']);
77-
78-
$schema['type'] = 'object';
79-
$schema['properties'] = [
80-
'_embedded' => [
81-
'anyOf' => [
82-
[
125+
if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) {
126+
$definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
127+
'type' => 'object',
128+
'properties' => [
129+
'_embedded' => [
130+
'anyOf' => [
131+
[
132+
'type' => 'object',
133+
'properties' => [
134+
'item' => [
135+
'type' => 'array',
136+
],
137+
],
138+
],
139+
['type' => 'object'],
140+
],
141+
],
142+
'totalItems' => [
143+
'type' => 'integer',
144+
'minimum' => 0,
145+
],
146+
'itemsPerPage' => [
147+
'type' => 'integer',
148+
'minimum' => 0,
149+
],
150+
'_links' => [
83151
'type' => 'object',
84152
'properties' => [
85-
'item' => [
86-
'type' => 'array',
87-
'items' => $items,
153+
'self' => [
154+
'type' => 'object',
155+
'properties' => self::HREF_PROP,
156+
],
157+
'first' => [
158+
'type' => 'object',
159+
'properties' => self::HREF_PROP,
160+
],
161+
'last' => [
162+
'type' => 'object',
163+
'properties' => self::HREF_PROP,
164+
],
165+
'next' => [
166+
'type' => 'object',
167+
'properties' => self::HREF_PROP,
168+
],
169+
'previous' => [
170+
'type' => 'object',
171+
'properties' => self::HREF_PROP,
88172
],
89173
],
90174
],
91-
['type' => 'object'],
92175
],
93-
],
94-
'totalItems' => [
95-
'type' => 'integer',
96-
'minimum' => 0,
97-
],
98-
'itemsPerPage' => [
99-
'type' => 'integer',
100-
'minimum' => 0,
101-
],
102-
'_links' => [
176+
'required' => ['_links', '_embedded'],
177+
];
178+
}
179+
180+
unset($schema['items']);
181+
unset($schema['type']);
182+
183+
$schema['description'] = "$definitionName collection.";
184+
$schema['allOf'] = [
185+
['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME],
186+
[
103187
'type' => 'object',
104188
'properties' => [
105-
'self' => [
106-
'type' => 'object',
107-
'properties' => self::HREF_PROP,
108-
],
109-
'first' => [
110-
'type' => 'object',
111-
'properties' => self::HREF_PROP,
112-
],
113-
'last' => [
114-
'type' => 'object',
115-
'properties' => self::HREF_PROP,
116-
],
117-
'next' => [
118-
'type' => 'object',
119-
'properties' => self::HREF_PROP,
120-
],
121-
'previous' => [
122-
'type' => 'object',
123-
'properties' => self::HREF_PROP,
189+
'_embedded' => [
190+
'additionalProperties' => [
191+
'type' => 'array',
192+
'items' => ['$ref' => $prefix.$definitionName],
193+
],
124194
],
125195
],
126196
],
127197
];
128-
$schema['required'] = [
129-
'_links',
130-
'_embedded',
131-
];
132198

133199
return $schema;
134200
}

0 commit comments

Comments
 (0)