Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions src/JsonApi/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
}

if (!$collectionKey) {
$definitions[$definitionName]['properties'] = $this->buildDefinitionPropertiesSchema($key, $inputOrOutputClass, $format, $type, $operation, $schema, []);
$definitions[$definitionName]['properties'] = $this->buildDefinitionPropertiesSchema($key, $definitionName, $inputOrOutputClass, $format, $type, $operation, $schema, []);
$schema['$ref'] = $prefix.$definitionName;

return $schema;
Expand Down Expand Up @@ -259,7 +259,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
unset($schema['items']);
unset($schema['type']);

$properties = $this->buildDefinitionPropertiesSchema($key, $inputOrOutputClass, $format, $type, $operation, $schema, []);
$properties = $this->buildDefinitionPropertiesSchema($key, $definitionName, $inputOrOutputClass, $format, $type, $operation, $schema, []);

$properties['data'] = [
'type' => 'array',
Expand All @@ -282,7 +282,7 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
}
}

private function buildDefinitionPropertiesSchema(string $key, string $propertyClass, string $format, string $type, ?Operation $operation, Schema $schema, ?array $serializerContext): array
private function buildDefinitionPropertiesSchema(string $key, string $documentDefinitionName, string $propertyClass, string $format, string $type, ?Operation $operation, Schema $schema, ?array $serializerContext): array
{
// Capture the resource's operation up front; the relationship loop below reassigns $operation.
$resourceOperation = $operation;
Expand Down Expand Up @@ -321,7 +321,7 @@ private function buildDefinitionPropertiesSchema(string $key, string $propertyCl
}
// keep one entry per related definition: a polymorphic relation targets several resource classes, all of which may appear in "included"
foreach (array_keys($refs) as $ref) {
$relatedDefinitions[$ref] = ['$ref' => $ref];
$relatedDefinitions[$ref.'.resource'] = ['$ref' => $ref.'.resource'];
}
// A relationship literally named "relationships"/"included" is prefixed at runtime too.
$relationshipName = ReservedAttributeNameConverter::JSON_API_RESERVED_ATTRIBUTES[$propertyName] ?? $propertyName;
Expand Down Expand Up @@ -374,12 +374,15 @@ private function buildDefinitionPropertiesSchema(string $key, string $propertyCl
// Only relax the requirement on the input schema; responses still always carry an `id`.
$required = Schema::TYPE_INPUT === $type && $resourceOperation instanceof HttpOperation && 'POST' === $resourceOperation->getMethod() ? ['type'] : ['type', 'id'];

$resourceDefinitionName = $documentDefinitionName.'.resource';
$definitions[$resourceDefinitionName] = [
'type' => 'object',
'properties' => $replacement,
'required' => $required,
];

return [
'data' => [
'type' => 'object',
'properties' => $replacement,
'required' => $required,
],
'data' => ['$ref' => $this->getSchemaUriPrefix($schema->getVersion()).$resourceDefinitionName],
] + $included;
}

Expand Down
6 changes: 4 additions & 2 deletions src/JsonApi/Tests/JsonSchema/InputDtoSchemaFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public function testInputSchemaDocumentsEmbeddedDtoCollectionAsAttribute(): void
$schemaFactory = $this->buildSchemaFactory($operation);

$schema = $schemaFactory->buildSchema(Order::class, 'jsonapi', Schema::TYPE_INPUT, $operation);
$data = $schema->getDefinitions()[$schema->getRootDefinitionKey()]['properties']['data'];
$this->assertSame(['$ref' => '#/definitions/'.$schema->getRootDefinitionKey().'.resource'], $schema->getDefinitions()[$schema->getRootDefinitionKey()]['properties']['data']);
$data = $schema->getDefinitions()[$schema->getRootDefinitionKey().'.resource'];

$this->assertArrayNotHasKey('relationships', $data['properties']);
$this->assertEqualsCanonicalizing(['reference', 'lines'], array_keys($data['properties']['attributes']['properties']));
Expand All @@ -55,7 +56,8 @@ public function testOutputSchemaKeepsResourceCollectionAsRelationship(): void
$schemaFactory = $this->buildSchemaFactory($operation);

$schema = $schemaFactory->buildSchema(Order::class, 'jsonapi', Schema::TYPE_OUTPUT, $operation);
$data = $schema->getDefinitions()[$schema->getRootDefinitionKey()]['properties']['data'];
$this->assertSame(['$ref' => '#/definitions/'.$schema->getRootDefinitionKey().'.resource'], $schema->getDefinitions()[$schema->getRootDefinitionKey()]['properties']['data']);
$data = $schema->getDefinitions()[$schema->getRootDefinitionKey().'.resource'];

$this->assertArrayHasKey('lines', $data['properties']['relationships']['properties']);
$this->assertArrayNotHasKey('lines', $data['properties']['attributes']['properties']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function testReservedAttributeNamesAreRenamedLikeTheResponse(): void
{
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
$attributes = $resultSchema['definitions'][$rootDefinitionKey]['properties']['data']['properties']['attributes']['properties'];
$attributes = $resultSchema['definitions'][$rootDefinitionKey.'.resource']['properties']['attributes']['properties'];

// Every reserved name must be documented under the prefixed key the ReservedAttributeNameConverter emits.
$this->assertArrayHasKey('_id', $attributes);
Expand Down
39 changes: 18 additions & 21 deletions src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function testHasRootDefinitionKeyBuildSchema(): void
$this->assertTrue(isset($definitions[$rootDefinitionKey]));
$this->assertTrue(isset($definitions[$rootDefinitionKey]['properties']));
$properties = $resultSchema['definitions'][$rootDefinitionKey]['properties'];
$this->assertArrayHasKey('data', $properties);
$this->assertSame(['$ref' => '#/definitions/'.$rootDefinitionKey.'.resource'], $properties['data']);
$this->assertEquals(
[
'type' => 'object',
Expand All @@ -128,7 +128,7 @@ public function testHasRootDefinitionKeyBuildSchema(): void
'id',
],
],
$properties['data']
$definitions[$rootDefinitionKey.'.resource']
);
}

Expand Down Expand Up @@ -162,16 +162,13 @@ public function testSchemaTypeBuildSchema(): void
$objectSchema = $resultSchema['allOf'][1];
$this->assertArrayHasKey('data', $objectSchema['properties']);

$this->assertArrayHasKey('items', $objectSchema['properties']['data']);
$this->assertArrayNotHasKey('$ref', $objectSchema['properties']['data']['items']['properties']['attributes']);
$this->assertSame('object', $objectSchema['properties']['data']['items']['properties']['attributes']['type']);
$this->assertSame(['$ref' => '#/definitions/Dummy.jsonapi.resource'], $objectSchema['properties']['data']['items']);

$properties = $objectSchema['properties'];
$this->assertArrayHasKey('data', $properties);
$this->assertArrayHasKey('items', $properties['data']);
$this->assertArrayHasKey('id', $properties['data']['items']['properties']);
$this->assertArrayHasKey('type', $properties['data']['items']['properties']);
$this->assertArrayHasKey('attributes', $properties['data']['items']['properties']);
$resource = $resultSchema['definitions']['Dummy.jsonapi.resource'];
$this->assertArrayNotHasKey('$ref', $resource['properties']['attributes']);
$this->assertSame('object', $resource['properties']['attributes']['type']);
$this->assertArrayHasKey('id', $resource['properties']);
$this->assertArrayHasKey('type', $resource['properties']);

$forcedCollection = $this->schemaFactory->buildSchema(Dummy::class, 'jsonapi', Schema::TYPE_OUTPUT, forceCollection: true);
$this->assertEquals($resultSchema['allOf'][0]['$ref'], $forcedCollection['allOf'][0]['$ref']);
Expand All @@ -194,8 +191,8 @@ public function testPostInputSchemaDoesNotRequireId(): void
$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();

$this->assertTrue(isset($definitions[$rootDefinitionKey]['properties']['data']));
$data = $definitions[$rootDefinitionKey]['properties']['data'];
$this->assertSame(['$ref' => '#/definitions/'.$rootDefinitionKey.'.resource'], $definitions[$rootDefinitionKey]['properties']['data']);
$data = $definitions[$rootDefinitionKey.'.resource'];
$this->assertArrayHasKey('required', $data);
$this->assertSame(['type'], $data['required']);
}
Expand All @@ -206,7 +203,7 @@ public function testPatchInputSchemaRequiresId(): void
$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();

$data = $definitions[$rootDefinitionKey]['properties']['data'];
$data = $definitions[$rootDefinitionKey.'.resource'];
$this->assertSame(['type', 'id'], $data['required']);
}

Expand All @@ -216,7 +213,7 @@ public function testPostOutputSchemaRequiresId(): void
$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();

$data = $definitions[$rootDefinitionKey]['properties']['data'];
$data = $definitions[$rootDefinitionKey.'.resource'];
$this->assertSame(['type', 'id'], $data['required']);
}

Expand All @@ -227,7 +224,7 @@ public function testRelationIsExcludedFromAttributes(): void

$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
$dataProperties = $definitions[$rootDefinitionKey]['properties']['data']['properties'];
$dataProperties = $definitions[$rootDefinitionKey.'.resource']['properties'];

$this->assertArrayHasKey('attributes', $dataProperties);
$this->assertArrayNotHasKey('$ref', $dataProperties['attributes']);
Expand All @@ -251,7 +248,7 @@ public function testRelationshipLinkageRequiresTypeAndId(): void

$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
$dataProperties = $definitions[$rootDefinitionKey]['properties']['data']['properties'];
$dataProperties = $definitions[$rootDefinitionKey.'.resource']['properties'];

// a resource identifier object MUST contain type and id, @see https://jsonapi.org/format/#document-resource-identifier-objects
$linkage = $dataProperties['relationships']['properties']['relatedDummy']['properties']['data']['oneOf'][1];
Expand All @@ -269,8 +266,8 @@ public function testIncludedListsAllPolymorphicRelationTargets(): void
$included = $definitions[$rootDefinitionKey]['properties']['included'];

$refs = array_column($included['items']['anyOf'], '$ref');
$this->assertContains('#/definitions/RelatedDummy.jsonapi', $refs);
$this->assertContains('#/definitions/OtherRelatedDummy.jsonapi', $refs, 'every target of a polymorphic relation must be listed in included');
$this->assertContains('#/definitions/RelatedDummy.jsonapi.resource', $refs);
$this->assertContains('#/definitions/OtherRelatedDummy.jsonapi.resource', $refs, 'every target of a polymorphic relation must be listed in included');
}

public function testIncludedReferencesNotExposedRelatedResource(): void
Expand All @@ -281,7 +278,7 @@ public function testIncludedReferencesNotExposedRelatedResource(): void
$definitions = $resultSchema->getDefinitions();
$included = $definitions[$resultSchema->getRootDefinitionKey()]['properties']['included'];

$this->assertSame([['$ref' => '#/definitions/RelatedDummy.jsonapi']], $included['items']['anyOf']);
$this->assertSame([['$ref' => '#/definitions/RelatedDummy.jsonapi.resource']], $included['items']['anyOf']);
$this->assertArrayHasKey('RelatedDummy.jsonapi', $definitions);
}

Expand All @@ -294,7 +291,7 @@ public function testIncludedListsExposedAndNotExposedTargets(): void
$included = $definitions[$resultSchema->getRootDefinitionKey()]['properties']['included'];

$this->assertEqualsCanonicalizing(
[['$ref' => '#/definitions/RelatedDummy.jsonapi'], ['$ref' => '#/definitions/OtherRelatedDummy.jsonapi']],
[['$ref' => '#/definitions/RelatedDummy.jsonapi.resource'], ['$ref' => '#/definitions/OtherRelatedDummy.jsonapi.resource']],
$included['items']['anyOf']
);
}
Expand Down
26 changes: 26 additions & 0 deletions tests/Functional/JsonApi/RelatedResourcesInclusionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

namespace ApiPlatform\Tests\Functional\JsonApi;

use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\Test\ApiTestCase;
use ApiPlatform\Test\Constraint\MatchesJsonSchema;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProperty;
Expand Down Expand Up @@ -55,6 +57,30 @@ protected function setUp(): void
$this->recreateSchema(self::getResources());
}

public function testIncludedItemsSchemaMatchesRuntimeResourceObject(): void
{
$this->seedDummyPropertyObjects(1);

$response = self::createClient()->request('GET', '/dummy_properties/1?include=group', [
'headers' => ['Accept' => 'application/vnd.api+json'],
]);

$this->assertResponseIsSuccessful();

$schemaFactory = self::getContainer()->get('api_platform.json_schema.schema_factory');
$schema = $schemaFactory->buildSchema(DummyProperty::class, 'jsonapi', Schema::TYPE_OUTPUT);
$definitions = $schema->getDefinitions();
$anyOf = $definitions[$schema->getRootDefinitionKey()]['properties']['included']['items']['anyOf'];

$this->assertNotEmpty($anyOf);

$includedSchema = new MatchesJsonSchema(['definitions' => $definitions, 'anyOf' => $anyOf]);
$included = $response->toArray()['included'][0];

$this->assertThat($included, $includedSchema);
$this->assertThat(['type' => 1] + $included, $this->logicalNot($includedSchema));
}

public function testIncludeManyToOneRelation(): void
{
$this->seedDummyPropertyObjects(3);
Expand Down
46 changes: 21 additions & 25 deletions tests/Functional/JsonSchema/JsonApiJsonSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public function testJsonApi(): void
{
$speciesSchema = $this->schemaFactory->buildSchema(Issue6317::class, 'jsonapi', Schema::TYPE_OUTPUT);
$this->assertEquals('#/definitions/Issue6317.jsonapi', $speciesSchema['$ref']);
$data = $speciesSchema['definitions']['Issue6317.jsonapi']['properties']['data'];
$this->assertSame(['$ref' => '#/definitions/Issue6317.jsonapi.resource'], $speciesSchema['definitions']['Issue6317.jsonapi']['properties']['data']);
$data = $speciesSchema['definitions']['Issue6317.jsonapi.resource'];
$this->assertSame('object', $data['type']);
$this->assertSame(['type' => 'string'], $data['properties']['id']);
$this->assertSame(['type' => 'string'], $data['properties']['type']);
Expand All @@ -82,40 +83,40 @@ public function testJsonApiIncludesSchemaForQuestion(): void

$questionSchema = $this->schemaFactory->buildSchema(Question::class, 'jsonapi', Schema::TYPE_OUTPUT);
$json = $questionSchema->getDefinitions();
$properties = $json['Question.jsonapi']['properties']['data']['properties'];
$properties = $json['Question.jsonapi.resource']['properties'];
$included = $json['Question.jsonapi']['properties']['included'];

$this->assertArrayHasKey('answer', $properties['relationships']['properties']);
$this->assertArrayHasKey('anyOf', $included['items']);
$this->assertCount(1, $included['items']['anyOf']);
$this->assertArrayHasKey('$ref', $included['items']['anyOf'][0]);
$this->assertSame('#/definitions/Answer.jsonapi', $included['items']['anyOf'][0]['$ref']);
$this->assertSame('#/definitions/Answer.jsonapi.resource', $included['items']['anyOf'][0]['$ref']);
}

public function testJsonApiIncludesSchemaForAnimalObservation(): void
{
$animalObservationSchema = $this->schemaFactory->buildSchema(AnimalObservation::class, 'jsonapi', Schema::TYPE_OUTPUT);
$json = $animalObservationSchema->getDefinitions();
$properties = $json['AnimalObservation.jsonapi']['properties']['data']['properties'];
$properties = $json['AnimalObservation.jsonapi.resource']['properties'];
$included = $json['AnimalObservation.jsonapi']['properties']['included'];

$this->assertArrayHasKey('individuals', $properties['relationships']['properties']);
$this->assertArrayHasKey('anyOf', $included['items']);
$this->assertCount(1, $included['items']['anyOf']);
$this->assertSame('#/definitions/Animal.jsonapi', $included['items']['anyOf'][0]['$ref']);
$this->assertSame('#/definitions/Animal.jsonapi.resource', $included['items']['anyOf'][0]['$ref']);
}

public function testJsonApiIncludesSchemaForAnimal(): void
{
$animalSchema = $this->schemaFactory->buildSchema(Animal::class, 'jsonapi', Schema::TYPE_OUTPUT);
$json = $animalSchema->getDefinitions();
$properties = $json['Animal.jsonapi']['properties']['data']['properties'];
$properties = $json['Animal.jsonapi.resource']['properties'];
$included = $json['Animal.jsonapi']['properties']['included'];

$this->assertArrayHasKey('species', $properties['relationships']['properties']);
$this->assertArrayHasKey('anyOf', $included['items']);
$this->assertCount(1, $included['items']['anyOf']);
$this->assertSame('#/definitions/Species.jsonapi', $included['items']['anyOf'][0]['$ref']);
$this->assertSame('#/definitions/Species.jsonapi.resource', $included['items']['anyOf'][0]['$ref']);
}

public function testJsonApiIncludesSchemaForSpecies(): void
Expand All @@ -131,30 +132,25 @@ public function testJsonApiIncludesSchemaForSpecies(): void
'properties' => [
'data' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
],
'type' => [
'type' => 'string',
],
'attributes' => [
'type' => 'object',
],
],
'required' => [
'type',
'id',
],
],
'items' => ['$ref' => '#/definitions/Species.jsonapi.resource'],
],
],
],
],
],
$speciesSchema->getArrayCopy()
);
$this->assertArraySubset(
[
'type' => 'object',
'properties' => [
'id' => ['type' => 'string'],
'type' => ['type' => 'string'],
'attributes' => ['type' => 'object'],
],
'required' => ['type', 'id'],
],
$speciesSchema->getDefinitions()['Species.jsonapi.resource']
);
}
}
Loading
Loading