Skip to content

Commit 79edced

Browse files
committed
fix(json-schema): share invariable sub-schemas
1 parent 4120709 commit 79edced

File tree

45 files changed

+1422
-670
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1422
-670
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,7 @@ jobs:
12981298
tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml
12991299
- name: Validate OpenAPI documents
13001300
run: |
1301-
npx @quobix/vacuum lint -r tests/Fixtures/app/ruleset.yaml build/out/openapi/openapi_v3.yaml -d
1301+
npx @quobix/vacuum lint -r tests/Fixtures/app/ruleset.yaml build/out/openapi/openapi_v3.yaml -d --ignore-array-circle-ref --ignore-polymorph-circle-ref -b --no-clip
13021302
13031303
laravel:
13041304
name: Laravel (PHP ${{ matrix.php }})

features/openapi/docs.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,12 @@ 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
"""
8787
{
8888
"default": "male",
89-
"example": "male",
9089
"type": ["string", "null"],
9190
"enum": [
9291
"male",
@@ -216,6 +215,7 @@ Feature: Documentation support
216215
And the "resourceRelated" property for the OpenAPI class "Resource" should be equal to:
217216
"""
218217
{
218+
"readOnly": true,
219219
"anyOf": [
220220
{
221221
"$ref": "#/components/schemas/ResourceRelated"

src/Hal/JsonSchema/SchemaFactory.php

Lines changed: 118 additions & 52 deletions
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)