-
-
Notifications
You must be signed in to change notification settings - Fork 966
fix(jsonapi): exclude relations from openapi attributes schema #8313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,9 +15,11 @@ | |
|
|
||
| use ApiPlatform\JsonApi\JsonSchema\SchemaFactory; | ||
| use ApiPlatform\JsonApi\Tests\Fixtures\Dummy; | ||
| use ApiPlatform\JsonApi\Tests\Fixtures\RelatedDummy; | ||
| use ApiPlatform\JsonSchema\DefinitionNameFactory; | ||
| use ApiPlatform\JsonSchema\Schema; | ||
| use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; | ||
| use ApiPlatform\Metadata\ApiProperty; | ||
| use ApiPlatform\Metadata\ApiResource; | ||
| use ApiPlatform\Metadata\Get; | ||
| use ApiPlatform\Metadata\GetCollection; | ||
|
|
@@ -31,7 +33,9 @@ | |
| use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; | ||
| use ApiPlatform\Metadata\ResourceClassResolverInterface; | ||
| use PHPUnit\Framework\TestCase; | ||
| use Prophecy\Argument; | ||
| use Prophecy\PhpUnit\ProphecyTrait; | ||
| use Symfony\Component\TypeInfo\Type; | ||
|
|
||
| class SchemaFactoryTest extends TestCase | ||
| { | ||
|
|
@@ -112,7 +116,8 @@ public function testHasRootDefinitionKeyBuildSchema(): void | |
| 'type' => 'string', | ||
| ], | ||
| 'attributes' => [ | ||
| '$ref' => '#/definitions/Dummy', | ||
| 'type' => 'object', | ||
| 'properties' => [], | ||
| ], | ||
| ], | ||
| 'required' => [ | ||
|
|
@@ -155,7 +160,8 @@ public function testSchemaTypeBuildSchema(): void | |
| $this->assertArrayHasKey('data', $objectSchema['properties']); | ||
|
|
||
| $this->assertArrayHasKey('items', $objectSchema['properties']['data']); | ||
| $this->assertArrayHasKey('$ref', $objectSchema['properties']['data']['items']['properties']['attributes']); | ||
| $this->assertArrayNotHasKey('$ref', $objectSchema['properties']['data']['items']['properties']['attributes']); | ||
| $this->assertSame('object', $objectSchema['properties']['data']['items']['properties']['attributes']['type']); | ||
|
|
||
| $properties = $objectSchema['properties']; | ||
| $this->assertArrayHasKey('data', $properties); | ||
|
|
@@ -199,4 +205,89 @@ public function testPostOutputSchemaRequiresId(): void | |
| $data = $definitions[$rootDefinitionKey]['properties']['data']; | ||
| $this->assertSame(['type', 'id'], $data['required']); | ||
| } | ||
|
|
||
| public function testRelationIsExcludedFromAttributes(): void | ||
| { | ||
| $schemaFactory = $this->buildSchemaFactoryWithRelation(); | ||
| $resultSchema = $schemaFactory->buildSchema(Dummy::class, 'jsonapi'); | ||
|
|
||
| $definitions = $resultSchema->getDefinitions(); | ||
| $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); | ||
| $dataProperties = $definitions[$rootDefinitionKey]['properties']['data']['properties']; | ||
|
|
||
| $this->assertArrayHasKey('attributes', $dataProperties); | ||
| $this->assertArrayNotHasKey('$ref', $dataProperties['attributes']); | ||
| $this->assertSame('object', $dataProperties['attributes']['type']); | ||
|
|
||
| $attributes = $dataProperties['attributes']['properties']; | ||
| $this->assertArrayHasKey('name', $attributes); | ||
| $this->assertArrayNotHasKey('relatedDummy', $attributes, 'relations must not be documented as attributes'); | ||
|
|
||
| $this->assertArrayHasKey('_id', $attributes, 'id is exposed as _id in the JSON:API attributes'); | ||
| $this->assertArrayNotHasKey('id', $attributes); | ||
|
|
||
| $this->assertArrayHasKey('relationships', $dataProperties); | ||
| $this->assertArrayHasKey('relatedDummy', $dataProperties['relationships']['properties']); | ||
| } | ||
|
|
||
| private function buildSchemaFactoryWithRelation(): SchemaFactory | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note: |
||
| { | ||
| $dummyOperation = (new Get())->withName('get')->withShortName('Dummy'); | ||
| $relatedOperation = (new Get())->withName('get')->withShortName('RelatedDummy'); | ||
|
|
||
| $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); | ||
| $resourceMetadataFactory->create(Dummy::class)->willReturn( | ||
| new ResourceMetadataCollection(Dummy::class, [ | ||
| (new ApiResource())->withOperations(new Operations(['get' => $dummyOperation])), | ||
| ]) | ||
| ); | ||
| $resourceMetadataFactory->create(RelatedDummy::class)->willReturn( | ||
| new ResourceMetadataCollection(RelatedDummy::class, [ | ||
| (new ApiResource())->withOperations(new Operations(['get' => $relatedOperation])), | ||
| ]) | ||
| ); | ||
|
|
||
| $propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class); | ||
| $propertyNameCollectionFactory->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['id', 'name', 'relatedDummy'])); | ||
| $propertyNameCollectionFactory->create(RelatedDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['id', 'name'])); | ||
|
|
||
| $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); | ||
| $propertyMetadataFactory->create(Dummy::class, 'id', Argument::type('array'))->willReturn( | ||
| (new ApiProperty())->withNativeType(Type::int())->withReadable(true)->withSchema(['type' => 'integer']) | ||
| ); | ||
| $propertyMetadataFactory->create(Dummy::class, 'name', Argument::type('array'))->willReturn( | ||
| (new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withSchema(['type' => 'string']) | ||
| ); | ||
| $propertyMetadataFactory->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn( | ||
| (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withReadable(true)->withSchema(['type' => Schema::UNKNOWN_TYPE]) | ||
| ); | ||
| $propertyMetadataFactory->create(RelatedDummy::class, 'id', Argument::type('array'))->willReturn( | ||
| (new ApiProperty())->withNativeType(Type::int())->withReadable(true)->withSchema(['type' => 'integer']) | ||
| ); | ||
| $propertyMetadataFactory->create(RelatedDummy::class, 'name', Argument::type('array'))->willReturn( | ||
| (new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withSchema(['type' => 'string']) | ||
| ); | ||
|
|
||
| $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); | ||
| $resourceClassResolver->isResourceClass(Dummy::class)->willReturn(true); | ||
| $resourceClassResolver->isResourceClass(RelatedDummy::class)->willReturn(true); | ||
|
|
||
| $definitionNameFactory = new DefinitionNameFactory(null); | ||
|
|
||
| $baseSchemaFactory = new BaseSchemaFactory( | ||
| resourceMetadataFactory: $resourceMetadataFactory->reveal(), | ||
| propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), | ||
| propertyMetadataFactory: $propertyMetadataFactory->reveal(), | ||
| resourceClassResolver: $resourceClassResolver->reveal(), | ||
| definitionNameFactory: $definitionNameFactory, | ||
| ); | ||
|
|
||
| return new SchemaFactory( | ||
| schemaFactory: $baseSchemaFactory, | ||
| propertyMetadataFactory: $propertyMetadataFactory->reveal(), | ||
| resourceClassResolver: $resourceClassResolver->reveal(), | ||
| resourceMetadataFactory: $resourceMetadataFactory->reveal(), | ||
| definitionNameFactory: $definitionNameFactory, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,28 +59,19 @@ public function testJsonApi(): void | |
| { | ||
| $speciesSchema = $this->schemaFactory->buildSchema(Issue6317::class, 'jsonapi', Schema::TYPE_OUTPUT); | ||
| $this->assertEquals('#/definitions/Issue6317.jsonapi', $speciesSchema['$ref']); | ||
| $this->assertEquals([ | ||
| 'properties' => [ | ||
| 'data' => [ | ||
| 'type' => 'object', | ||
| 'properties' => [ | ||
| 'id' => [ | ||
| 'type' => 'string', | ||
| ], | ||
| 'type' => [ | ||
| 'type' => 'string', | ||
| ], | ||
| 'attributes' => [ | ||
| '$ref' => '#/definitions/Issue6317', | ||
| ], | ||
| ], | ||
| 'required' => [ | ||
| 'type', | ||
| 'id', | ||
| ], | ||
| ], | ||
| ], | ||
| ], $speciesSchema['definitions']['Issue6317.jsonapi']); | ||
| $data = $speciesSchema['definitions']['Issue6317.jsonapi']['properties']['data']; | ||
| $this->assertSame('object', $data['type']); | ||
| $this->assertSame(['type' => 'string'], $data['properties']['id']); | ||
| $this->assertSame(['type' => 'string'], $data['properties']['type']); | ||
| $this->assertSame(['type', 'id'], $data['required']); | ||
|
|
||
| $attributes = $data['properties']['attributes']; | ||
| $this->assertArrayNotHasKey('$ref', $attributes); | ||
| $this->assertSame('object', $attributes['type']); | ||
| $this->assertEqualsCanonicalizing( | ||
| ['name', 'value', '_id', 'ordinal', 'cardinal'], | ||
| array_keys($attributes['properties']) | ||
| ); | ||
| } | ||
|
|
||
| public function testJsonApiIncludesSchemaForQuestion(): void | ||
|
|
@@ -150,7 +141,7 @@ public function testJsonApiIncludesSchemaForSpecies(): void | |
| 'type' => 'string', | ||
| ], | ||
| 'attributes' => [ | ||
| '$ref' => '#/definitions/Species', | ||
| 'type' => 'object', | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only assertion on the collection path's attributes (the path the production change at the top of Consider asserting the collection item's |
||
| ], | ||
| ], | ||
| 'required' => [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reserved-name handling is incomplete. The attribute loop above (lines 341-346) only renames
id→_id, but the runtimeReservedAttributeNameConverter::JSON_API_RESERVED_ATTRIBUTESrenames five names:The base JSON schema is built with the generic
api_platform.name_converter(json_schema.php:31), not the reserved converter (which is wired only into the JSON:API normalizers, jsonapi.php:69), so a resource property literally namedtype(a common field name) arrives here astypeand is emitted in the schema asattributes.properties.type. At runtimeItemNormalizerrenames it to_type, so the documented key never matches the response — the same doc/response mismatch this PR is fixing for relations. Since the schema now enumerates attribute keys, this is observable.Suggest sourcing the rename from the converter's map instead of hard-coding the
idcase, e.g.:(and dropping the
if ('id' === $propertyName)special case). A fixture with a property namedtypewould also lock this down — the new test only exercisesid.