Skip to content

Commit aeca014

Browse files
ttskchsoyuka
andauthored
feat(jsonschema): make JSON-LD specific properties required (#6366)
* feat(jsonschema): for JSON-LD, output the input and output schemas with different keys * feat(jsonschema): make JSON-LD specific properties required * fix exsistent tests * Update src/Hydra/JsonSchema/SchemaFactory.php Co-authored-by: Antoine Bluchet <[email protected]> * refactor: reduce complexity * docs: remove unnecessary comments * fix: make JSON-LD specific properties non-readOnly and make them required only in output schema --------- Co-authored-by: Antoine Bluchet <[email protected]>
1 parent 53e6ce7 commit aeca014

File tree

4 files changed

+97
-30
lines changed

4 files changed

+97
-30
lines changed

features/openapi/docs.feature

+4-4
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ Feature: Documentation support
153153
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 6 elements
154154

155155
# Subcollection - check schema
156-
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"
156+
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.output"
157157

158158
# Deprecations
159159
And the JSON node "paths./dummies.get.deprecated" should be false
@@ -165,8 +165,8 @@ Feature: Documentation support
165165
And the JSON node "paths./deprecated_resources/{id}.patch.deprecated" should be true
166166

167167
# Formats
168-
And the OpenAPI class "Dummy.jsonld" exists
169-
And the "@id" property exists for the OpenAPI class "Dummy.jsonld"
168+
And the OpenAPI class "Dummy.jsonld.output" exists
169+
And the "@id" property exists for the OpenAPI class "Dummy.jsonld.output"
170170
And the JSON node "paths./dummies.get.responses.200.content.application/ld+json" should be equal to:
171171
"""
172172
{
@@ -176,7 +176,7 @@ Feature: Documentation support
176176
"hydra:member": {
177177
"type": "array",
178178
"items": {
179-
"$ref": "#/components/schemas/Dummy.jsonld"
179+
"$ref": "#/components/schemas/Dummy.jsonld.output"
180180
}
181181
},
182182
"hydra:totalItems": {

src/Hydra/JsonSchema/SchemaFactory.php

+27-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
2828
{
2929
private const BASE_PROP = [
30-
'readOnly' => true,
3130
'type' => 'string',
3231
];
3332
private const BASE_PROPS = [
@@ -36,7 +35,6 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
3635
];
3736
private const BASE_ROOT_PROPS = [
3837
'@context' => [
39-
'readOnly' => true,
4038
'oneOf' => [
4139
['type' => 'string'],
4240
[
@@ -74,18 +72,43 @@ public function buildSchema(string $className, string $format = 'jsonld', string
7472
return $schema;
7573
}
7674

77-
if ('input' === $type) {
78-
return $schema;
75+
if (($key = $schema->getRootDefinitionKey() ?? $schema->getItemsDefinitionKey()) !== null) {
76+
$postfix = '.'.$type;
77+
$definitions = $schema->getDefinitions();
78+
$definitions[$key.$postfix] = $definitions[$key];
79+
unset($definitions[$key]);
80+
81+
if (($schema['type'] ?? '') === 'array') {
82+
$schema['items']['$ref'] .= $postfix;
83+
} else {
84+
$schema['$ref'] .= $postfix;
85+
}
7986
}
8087

8188
$definitions = $schema->getDefinitions();
8289
if ($key = $schema->getRootDefinitionKey()) {
8390
$definitions[$key]['properties'] = self::BASE_ROOT_PROPS + ($definitions[$key]['properties'] ?? []);
91+
if (Schema::TYPE_OUTPUT === $type) {
92+
foreach (array_keys(self::BASE_ROOT_PROPS) as $property) {
93+
$definitions[$key]['required'] ??= [];
94+
if (!\in_array($property, $definitions[$key]['required'], true)) {
95+
$definitions[$key]['required'][] = $property;
96+
}
97+
}
98+
}
8499

85100
return $schema;
86101
}
87102
if ($key = $schema->getItemsDefinitionKey()) {
88103
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
104+
if (Schema::TYPE_OUTPUT === $type) {
105+
foreach (array_keys(self::BASE_PROPS) as $property) {
106+
$definitions[$key]['required'] ??= [];
107+
if (!\in_array($property, $definitions[$key]['required'], true)) {
108+
$definitions[$key]['required'][] = $property;
109+
}
110+
}
111+
}
89112
}
90113

91114
if (($schema['type'] ?? '') === 'array') {

tests/Hydra/JsonSchema/SchemaFactoryTest.php

+47-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\Metadata\ApiResource;
2222
use ApiPlatform\Metadata\Get;
2323
use ApiPlatform\Metadata\GetCollection;
24+
use ApiPlatform\Metadata\Post;
2425
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2526
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2627
use ApiPlatform\Metadata\Property\PropertyNameCollection;
@@ -49,6 +50,7 @@ protected function setUp(): void
4950

5051
$propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
5152
$propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT])->willReturn(new PropertyNameCollection());
53+
$propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_INPUT])->willReturn(new PropertyNameCollection());
5254
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
5355

5456
$definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]);
@@ -69,7 +71,12 @@ public function testBuildSchema(): void
6971
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);
7072

7173
$this->assertTrue($resultSchema->isDefined());
72-
$this->assertSame('Dummy.jsonld', $resultSchema->getRootDefinitionKey());
74+
$this->assertSame('Dummy.jsonld.output', $resultSchema->getRootDefinitionKey());
75+
76+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_INPUT, new Post());
77+
78+
$this->assertTrue($resultSchema->isDefined());
79+
$this->assertSame('Dummy.jsonld.input', $resultSchema->getRootDefinitionKey());
7380
}
7481

7582
public function testCustomFormatBuildSchema(): void
@@ -94,7 +101,6 @@ public function testHasRootDefinitionKeyBuildSchema(): void
94101
$this->assertArrayHasKey('@context', $properties);
95102
$this->assertEquals(
96103
[
97-
'readOnly' => true,
98104
'oneOf' => [
99105
['type' => 'string'],
100106
[
@@ -122,7 +128,7 @@ public function testHasRootDefinitionKeyBuildSchema(): void
122128
public function testSchemaTypeBuildSchema(): void
123129
{
124130
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection());
125-
$definitionName = 'Dummy.jsonld';
131+
$definitionName = 'Dummy.jsonld.output';
126132

127133
$this->assertNull($resultSchema->getRootDefinitionKey());
128134
// @noRector
@@ -151,6 +157,12 @@ public function testSchemaTypeBuildSchema(): void
151157
$this->assertArrayNotHasKey('@context', $properties);
152158
$this->assertArrayHasKey('@type', $properties);
153159
$this->assertArrayHasKey('@id', $properties);
160+
161+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_INPUT, new Post());
162+
$definitionName = 'Dummy.jsonld.input';
163+
164+
$this->assertSame($definitionName, $resultSchema->getRootDefinitionKey());
165+
$this->assertFalse(isset($resultSchema['properties']));
154166
}
155167

156168
public function testHasHydraViewNavigationBuildSchema(): void
@@ -168,4 +180,36 @@ public function testHasHydraViewNavigationBuildSchema(): void
168180
$this->assertArrayHasKey('hydra:previous', $resultSchema['properties']['hydra:view']['properties']);
169181
$this->assertArrayHasKey('hydra:next', $resultSchema['properties']['hydra:view']['properties']);
170182
}
183+
184+
public function testRequiredBasePropertiesBuildSchema(): void
185+
{
186+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);
187+
$definitions = $resultSchema->getDefinitions();
188+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
189+
190+
$this->assertTrue(isset($definitions[$rootDefinitionKey]));
191+
$this->assertTrue(isset($definitions[$rootDefinitionKey]['required']));
192+
$requiredProperties = $resultSchema['definitions'][$rootDefinitionKey]['required'];
193+
$this->assertContains('@context', $requiredProperties);
194+
$this->assertContains('@id', $requiredProperties);
195+
$this->assertContains('@type', $requiredProperties);
196+
197+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection());
198+
$definitions = $resultSchema->getDefinitions();
199+
$itemsDefinitionKey = array_key_first($definitions->getArrayCopy());
200+
201+
$this->assertTrue(isset($definitions[$itemsDefinitionKey]));
202+
$this->assertTrue(isset($definitions[$itemsDefinitionKey]['required']));
203+
$requiredProperties = $resultSchema['definitions'][$itemsDefinitionKey]['required'];
204+
$this->assertNotContains('@context', $requiredProperties);
205+
$this->assertContains('@id', $requiredProperties);
206+
$this->assertContains('@type', $requiredProperties);
207+
208+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_INPUT, new Post());
209+
$definitions = $resultSchema->getDefinitions();
210+
$itemsDefinitionKey = array_key_first($definitions->getArrayCopy());
211+
212+
$this->assertTrue(isset($definitions[$itemsDefinitionKey]));
213+
$this->assertFalse(isset($definitions[$itemsDefinitionKey]['required']));
214+
}
171215
}

tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php

+19-19
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ public function testExecuteWithJsonldTypeInput(): void
7676
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld', '--type' => 'input']);
7777
$result = $this->tester->getDisplay();
7878

79-
$this->assertStringNotContainsString('@id', $result);
80-
$this->assertStringNotContainsString('@context', $result);
81-
$this->assertStringNotContainsString('@type', $result);
79+
$this->assertStringContainsString('@id', $result);
80+
$this->assertStringContainsString('@context', $result);
81+
$this->assertStringContainsString('@type', $result);
8282
}
8383

8484
/**
@@ -103,24 +103,24 @@ public function testArraySchemaWithReference(): void
103103
$result = $this->tester->getDisplay();
104104
$json = json_decode($result, associative: true);
105105

106-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['tests'], [
106+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['tests'], [
107107
'type' => 'string',
108108
'foo' => 'bar',
109109
]);
110110

111-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['nonResourceTests'], [
111+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['nonResourceTests'], [
112112
'type' => 'array',
113113
'items' => [
114-
'$ref' => '#/definitions/NonResourceTestEntity.jsonld-write',
114+
'$ref' => '#/definitions/NonResourceTestEntity.jsonld-write.input',
115115
],
116116
]);
117117

118-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['description'], [
118+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['description'], [
119119
'maxLength' => 255,
120120
]);
121121

122-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['type'], [
123-
'$ref' => '#/definitions/TestEntity.jsonld-write',
122+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['type'], [
123+
'$ref' => '#/definitions/TestEntity.jsonld-write.input',
124124
]);
125125
}
126126

@@ -130,14 +130,14 @@ public function testArraySchemaWithMultipleUnionTypesJsonLd(): void
130130
$result = $this->tester->getDisplay();
131131
$json = json_decode($result, associative: true);
132132

133-
$this->assertEquals($json['definitions']['Nest.jsonld']['properties']['owner']['anyOf'], [
134-
['$ref' => '#/definitions/Wren.jsonld'],
135-
['$ref' => '#/definitions/Robin.jsonld'],
133+
$this->assertEquals($json['definitions']['Nest.jsonld.output']['properties']['owner']['anyOf'], [
134+
['$ref' => '#/definitions/Wren.jsonld.output'],
135+
['$ref' => '#/definitions/Robin.jsonld.output'],
136136
['type' => 'null'],
137137
]);
138138

139-
$this->assertArrayHasKey('Wren.jsonld', $json['definitions']);
140-
$this->assertArrayHasKey('Robin.jsonld', $json['definitions']);
139+
$this->assertArrayHasKey('Wren.jsonld.output', $json['definitions']);
140+
$this->assertArrayHasKey('Robin.jsonld.output', $json['definitions']);
141141
}
142142

143143
public function testArraySchemaWithMultipleUnionTypesJsonApi(): void
@@ -183,7 +183,7 @@ public function testArraySchemaWithTypeFactory(): void
183183
$result = $this->tester->getDisplay();
184184
$json = json_decode($result, associative: true);
185185

186-
$this->assertEquals($json['definitions']['Foo.jsonld']['properties']['expiration'], ['type' => 'string', 'format' => 'date']);
186+
$this->assertEquals($json['definitions']['Foo.jsonld.output']['properties']['expiration'], ['type' => 'string', 'format' => 'date']);
187187
}
188188

189189
/**
@@ -195,7 +195,7 @@ public function testWritableNonResourceRef(): void
195195
$result = $this->tester->getDisplay();
196196
$json = json_decode($result, associative: true);
197197

198-
$this->assertEquals($json['definitions']['SaveProduct.jsonld']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld');
198+
$this->assertEquals($json['definitions']['SaveProduct.jsonld.input']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld.input');
199199
}
200200

201201
/**
@@ -207,8 +207,8 @@ public function testOpenApiResourceRefIsNotOverwritten(): void
207207
$result = $this->tester->getDisplay();
208208
$json = json_decode($result, associative: true);
209209

210-
$this->assertEquals('#/definitions/DummyFriend', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld']['properties']['itemDto']['$ref']);
211-
$this->assertEquals('#/definitions/DummyDate', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld']['properties']['collectionDto']['items']['$ref']);
210+
$this->assertEquals('#/definitions/DummyFriend', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld.output']['properties']['itemDto']['$ref']);
211+
$this->assertEquals('#/definitions/DummyDate', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld.output']['properties']['collectionDto']['items']['$ref']);
212212
}
213213

214214
/**
@@ -220,7 +220,7 @@ public function testSubSchemaJsonLd(): void
220220
$result = $this->tester->getDisplay();
221221
$json = json_decode($result, associative: true);
222222

223-
$this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends']['properties']);
223+
$this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends.output']['properties']);
224224
}
225225

226226
public function testJsonApiIncludesSchema(): void

0 commit comments

Comments
 (0)