Skip to content

Commit 1056410

Browse files
arnedesmedtcoudenysjalanpoulain
authored
feat: add a json schema builder for HAL (based on Hydra) (#4357)
Co-authored-by: Jachim Coudenys <[email protected]> Co-authored-by: Alan Poulain <[email protected]>
1 parent bf9d9c8 commit 1056410

File tree

6 files changed

+287
-4
lines changed

6 files changed

+287
-4
lines changed

src/Core/Bridge/Symfony/Bundle/Resources/config/hal.xml

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@
5454
<!-- Run after serializer.denormalizer.array but before serializer.normalizer.object -->
5555
<tag name="serializer.normalizer" priority="-995" />
5656
</service>
57+
58+
<!-- JSON Schema -->
59+
60+
<service id="api_platform.hal.json_schema.schema_factory" class="ApiPlatform\Core\Hal\JsonSchema\SchemaFactory" decorates="api_platform.json_schema.schema_factory">
61+
<argument type="service" id="api_platform.hal.json_schema.schema_factory.inner" />
62+
</service>
5763
</services>
5864

5965
</container>
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Hal\JsonSchema;
15+
16+
use ApiPlatform\Core\JsonSchema\Schema;
17+
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
18+
19+
/**
20+
* Decorator factory which adds HAL properties to the JSON Schema document.
21+
*
22+
* @experimental
23+
*
24+
* @author Kévin Dunglas <[email protected]>
25+
* @author Jachim Coudenys <[email protected]>
26+
*/
27+
final class SchemaFactory implements SchemaFactoryInterface
28+
{
29+
private const HREF_PROP = [
30+
'href' => [
31+
'type' => 'string',
32+
'format' => 'iri-reference',
33+
],
34+
];
35+
private const BASE_PROPS = [
36+
'_links' => [
37+
'type' => 'object',
38+
'properties' => [
39+
'self' => [
40+
'type' => 'object',
41+
'properties' => self::HREF_PROP,
42+
],
43+
],
44+
],
45+
];
46+
47+
private $schemaFactory;
48+
49+
public function __construct(SchemaFactoryInterface $schemaFactory)
50+
{
51+
$this->schemaFactory = $schemaFactory;
52+
53+
$this->addDistinctFormat('jsonhal');
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function buildSchema(string $className, string $format = 'jsonhal', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
60+
{
61+
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
62+
if ('jsonhal' !== $format) {
63+
return $schema;
64+
}
65+
66+
$definitions = $schema->getDefinitions();
67+
if ($key = $schema->getRootDefinitionKey()) {
68+
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
69+
70+
return $schema;
71+
}
72+
if ($key = $schema->getItemsDefinitionKey()) {
73+
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
74+
}
75+
76+
if (($schema['type'] ?? '') === 'array') {
77+
$items = $schema['items'];
78+
unset($schema['items']);
79+
80+
$schema['type'] = 'object';
81+
$schema['properties'] = [
82+
'_embedded' => [
83+
'type' => 'array',
84+
'items' => $items,
85+
],
86+
'totalItems' => [
87+
'type' => 'integer',
88+
'minimum' => 0,
89+
],
90+
'itemsPerPage' => [
91+
'type' => 'integer',
92+
'minimum' => 0,
93+
],
94+
'_links' => [
95+
'type' => 'object',
96+
'properties' => [
97+
'self' => [
98+
'type' => 'object',
99+
'properties' => self::HREF_PROP,
100+
],
101+
'first' => [
102+
'type' => 'object',
103+
'properties' => self::HREF_PROP,
104+
],
105+
'last' => [
106+
'type' => 'object',
107+
'properties' => self::HREF_PROP,
108+
],
109+
'next' => [
110+
'type' => 'object',
111+
'properties' => self::HREF_PROP,
112+
],
113+
'previous' => [
114+
'type' => 'object',
115+
'properties' => self::HREF_PROP,
116+
],
117+
],
118+
],
119+
];
120+
$schema['required'] = [
121+
'_links',
122+
'_embedded',
123+
];
124+
125+
return $schema;
126+
}
127+
128+
return $schema;
129+
}
130+
131+
public function addDistinctFormat(string $format): void
132+
{
133+
if (method_exists($this->schemaFactory, 'addDistinctFormat')) {
134+
$this->schemaFactory->addDistinctFormat($format);
135+
}
136+
}
137+
}

src/Core/Hydra/JsonSchema/SchemaFactory.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ public function __construct(SchemaFactoryInterface $schemaFactory)
6464
{
6565
$this->schemaFactory = $schemaFactory;
6666

67-
if ($schemaFactory instanceof BaseSchemaFactory) {
68-
$schemaFactory->addDistinctFormat('jsonld');
69-
}
67+
$this->addDistinctFormat('jsonld');
7068
}
7169

7270
/**
@@ -173,4 +171,11 @@ public function buildSchema(string $className, string $format = 'jsonld', string
173171

174172
return $schema;
175173
}
174+
175+
public function addDistinctFormat(string $format): void
176+
{
177+
if ($this->schemaFactory instanceof BaseSchemaFactory) {
178+
$this->schemaFactory->addDistinctFormat($format);
179+
}
180+
}
176181
}

tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
12661266
{
12671267
$hasSwagger = null === $configuration || true === $configuration['api_platform']['enable_swagger'] ?? false;
12681268
$hasHydra = null === $configuration || isset($configuration['api_platform']['formats']['jsonld']);
1269+
$hasHal = null === $configuration || isset($configuration['api_platform']['formats']['jsonhal']);
12691270

12701271
$containerBuilderProphecy = $this->getPartialContainerBuilderProphecy($configuration);
12711272

@@ -1607,6 +1608,10 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
16071608
$definitions[] = 'api_platform.jsonld.normalizer.object';
16081609
}
16091610

1611+
if ($hasHal) {
1612+
$definitions[] = 'api_platform.hal.json_schema.schema_factory';
1613+
}
1614+
16101615
// Ignore inlined services
16111616
$containerBuilderProphecy->setDefinition(Argument::that(static function (string $arg) {
16121617
return 0 === strpos($arg, '.');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Hal\JsonSchema;
15+
16+
use ApiPlatform\Core\Api\OperationType;
17+
use ApiPlatform\Core\Hal\JsonSchema\SchemaFactory;
18+
use ApiPlatform\Core\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
19+
use ApiPlatform\Core\JsonSchema\Schema;
20+
use ApiPlatform\Core\JsonSchema\SchemaFactory as BaseSchemaFactory;
21+
use ApiPlatform\Core\JsonSchema\TypeFactoryInterface;
22+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
23+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
24+
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
25+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
26+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
27+
use ApiPlatform\Core\Tests\ProphecyTrait;
28+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
29+
use PHPUnit\Framework\TestCase;
30+
31+
/**
32+
* @group legacy
33+
*/
34+
class SchemaFactoryTest extends TestCase
35+
{
36+
use ProphecyTrait;
37+
38+
private $schemaFactory;
39+
40+
protected function setUp(): void
41+
{
42+
$typeFactory = $this->prophesize(TypeFactoryInterface::class);
43+
$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
44+
$resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata(Dummy::class));
45+
$propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
46+
$propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true])->willReturn(new PropertyNameCollection());
47+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
48+
49+
$baseSchemaFactory = new BaseSchemaFactory(
50+
$typeFactory->reveal(),
51+
$resourceMetadataFactory->reveal(),
52+
$propertyNameCollectionFactory->reveal(),
53+
$propertyMetadataFactory->reveal()
54+
);
55+
56+
$hydraSchemaFactory = new HydraSchemaFactory($baseSchemaFactory);
57+
58+
$this->schemaFactory = new SchemaFactory($hydraSchemaFactory);
59+
}
60+
61+
public function testBuildSchema(): void
62+
{
63+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);
64+
65+
$this->assertTrue($resultSchema->isDefined());
66+
$this->assertEquals(str_replace('\\', '.', Dummy::class).'.jsonhal', $resultSchema->getRootDefinitionKey());
67+
}
68+
69+
public function testCustomFormatBuildSchema(): void
70+
{
71+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json');
72+
73+
$this->assertTrue($resultSchema->isDefined());
74+
$this->assertEquals(str_replace('\\', '.', Dummy::class), $resultSchema->getRootDefinitionKey());
75+
}
76+
77+
public function testHasRootDefinitionKeyBuildSchema(): void
78+
{
79+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);
80+
$definitions = $resultSchema->getDefinitions();
81+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
82+
83+
$this->assertArrayHasKey($rootDefinitionKey, $definitions);
84+
$this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]);
85+
$properties = $resultSchema['definitions'][$rootDefinitionKey]['properties'];
86+
$this->assertArrayHasKey('_links', $properties);
87+
$this->assertSame(
88+
[
89+
'type' => 'object',
90+
'properties' => [
91+
'self' => [
92+
'type' => 'object',
93+
'properties' => [
94+
'href' => [
95+
'type' => 'string',
96+
'format' => 'iri-reference',
97+
],
98+
],
99+
],
100+
],
101+
],
102+
$properties['_links']
103+
);
104+
}
105+
106+
public function testSchemaTypeBuildSchema(): void
107+
{
108+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, OperationType::COLLECTION);
109+
$definitionName = str_replace('\\', '.', Dummy::class).'.jsonhal';
110+
111+
$this->assertNull($resultSchema->getRootDefinitionKey());
112+
$this->assertArrayHasKey('properties', $resultSchema);
113+
$this->assertArrayHasKey('_embedded', $resultSchema['properties']);
114+
$this->assertArrayHasKey('totalItems', $resultSchema['properties']);
115+
$this->assertArrayHasKey('itemsPerPage', $resultSchema['properties']);
116+
$this->assertArrayHasKey('_links', $resultSchema['properties']);
117+
$properties = $resultSchema['definitions'][$definitionName]['properties'];
118+
$this->assertArrayHasKey('_links', $properties);
119+
120+
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, null, null, null, null, true);
121+
122+
$this->assertNull($resultSchema->getRootDefinitionKey());
123+
$this->assertArrayHasKey('properties', $resultSchema);
124+
$this->assertArrayHasKey('_embedded', $resultSchema['properties']);
125+
$this->assertArrayHasKey('totalItems', $resultSchema['properties']);
126+
$this->assertArrayHasKey('itemsPerPage', $resultSchema['properties']);
127+
$this->assertArrayHasKey('_links', $resultSchema['properties']);
128+
$properties = $resultSchema['definitions'][$definitionName]['properties'];
129+
$this->assertArrayHasKey('_links', $properties);
130+
}
131+
}

tests/Core/Hydra/JsonSchema/SchemaFactoryTest.php

-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ public function testHasRootDefinitionKeyBuildSchema(): void
7878
$definitions = $resultSchema->getDefinitions();
7979
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
8080

81-
$this->assertEquals(str_replace('\\', '.', Dummy::class).'.jsonld', $rootDefinitionKey);
8281
$this->assertArrayHasKey($rootDefinitionKey, $definitions);
8382
$this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]);
8483
$properties = $resultSchema['definitions'][$rootDefinitionKey]['properties'];

0 commit comments

Comments
 (0)