Skip to content

Hal jsonschema #4357

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

Merged
merged 13 commits into from
Sep 24, 2021
6 changes: 6 additions & 0 deletions src/Core/Bridge/Symfony/Bundle/Resources/config/hal.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
<!-- Run after serializer.denormalizer.array but before serializer.normalizer.object -->
<tag name="serializer.normalizer" priority="-995" />
</service>

<!-- JSON Schema -->

<service id="api_platform.hal.json_schema.schema_factory" class="ApiPlatform\Core\Hal\JsonSchema\SchemaFactory" decorates="api_platform.json_schema.schema_factory">
<argument type="service" id="api_platform.hal.json_schema.schema_factory.inner" />
</service>
</services>

</container>
137 changes: 137 additions & 0 deletions src/Core/Hal/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Hal\JsonSchema;

use ApiPlatform\Core\JsonSchema\Schema;
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;

/**
* Decorator factory which adds HAL properties to the JSON Schema document.
*
* @experimental
*
* @author Kévin Dunglas <[email protected]>
* @author Jachim Coudenys <[email protected]>
*/
final class SchemaFactory implements SchemaFactoryInterface
{
private const HREF_PROP = [
'href' => [
'type' => 'string',
'format' => 'iri-reference',
],
];
private const BASE_PROPS = [
'_links' => [
'type' => 'object',
'properties' => [
'self' => [
'type' => 'object',
'properties' => self::HREF_PROP,
],
],
],
];

private $schemaFactory;

public function __construct(SchemaFactoryInterface $schemaFactory)
{
$this->schemaFactory = $schemaFactory;

$this->addDistinctFormat('jsonhal');
}

/**
* {@inheritdoc}
*/
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
{
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
if ('jsonhal' !== $format) {
return $schema;
}

$definitions = $schema->getDefinitions();
if ($key = $schema->getRootDefinitionKey()) {
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);

return $schema;
}
if ($key = $schema->getItemsDefinitionKey()) {
$definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
}

if (($schema['type'] ?? '') === 'array') {
$items = $schema['items'];
unset($schema['items']);

$schema['type'] = 'object';
$schema['properties'] = [
'_embedded' => [
'type' => 'array',
'items' => $items,
],
'totalItems' => [
'type' => 'integer',
'minimum' => 0,
],
'itemsPerPage' => [
'type' => 'integer',
'minimum' => 0,
],
'_links' => [
'type' => 'object',
'properties' => [
'self' => [
'type' => 'object',
'properties' => self::HREF_PROP,
],
'first' => [
'type' => 'object',
'properties' => self::HREF_PROP,
],
'last' => [
'type' => 'object',
'properties' => self::HREF_PROP,
],
'next' => [
'type' => 'object',
'properties' => self::HREF_PROP,
],
'previous' => [
'type' => 'object',
'properties' => self::HREF_PROP,
],
],
],
];
$schema['required'] = [
'_links',
'_embedded',
];

return $schema;
}

return $schema;
}

public function addDistinctFormat(string $format): void
{
if (method_exists($this->schemaFactory, 'addDistinctFormat')) {
$this->schemaFactory->addDistinctFormat($format);
}
}
}
11 changes: 8 additions & 3 deletions src/Core/Hydra/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ public function __construct(SchemaFactoryInterface $schemaFactory)
{
$this->schemaFactory = $schemaFactory;

if ($schemaFactory instanceof BaseSchemaFactory) {
$schemaFactory->addDistinctFormat('jsonld');
}
$this->addDistinctFormat('jsonld');
}

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

return $schema;
}

public function addDistinctFormat(string $format): void
{
if ($this->schemaFactory instanceof BaseSchemaFactory) {
$this->schemaFactory->addDistinctFormat($format);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
{
$hasSwagger = null === $configuration || true === $configuration['api_platform']['enable_swagger'] ?? false;
$hasHydra = null === $configuration || isset($configuration['api_platform']['formats']['jsonld']);
$hasHal = null === $configuration || isset($configuration['api_platform']['formats']['jsonhal']);

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

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

if ($hasHal) {
$definitions[] = 'api_platform.hal.json_schema.schema_factory';
}

// Ignore inlined services
$containerBuilderProphecy->setDefinition(Argument::that(static function (string $arg) {
return 0 === strpos($arg, '.');
Expand Down
131 changes: 131 additions & 0 deletions tests/Core/Hal/JsonSchema/SchemaFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Tests\Hal\JsonSchema;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Hal\JsonSchema\SchemaFactory;
use ApiPlatform\Core\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
use ApiPlatform\Core\JsonSchema\Schema;
use ApiPlatform\Core\JsonSchema\SchemaFactory as BaseSchemaFactory;
use ApiPlatform\Core\JsonSchema\TypeFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyNameCollection;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\ProphecyTrait;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
use PHPUnit\Framework\TestCase;

/**
* @group legacy
*/
class SchemaFactoryTest extends TestCase
{
use ProphecyTrait;

private $schemaFactory;

protected function setUp(): void
{
$typeFactory = $this->prophesize(TypeFactoryInterface::class);
$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactory->create(Dummy::class)->willReturn(new ResourceMetadata(Dummy::class));
$propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true])->willReturn(new PropertyNameCollection());
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);

$baseSchemaFactory = new BaseSchemaFactory(
$typeFactory->reveal(),
$resourceMetadataFactory->reveal(),
$propertyNameCollectionFactory->reveal(),
$propertyMetadataFactory->reveal()
);

$hydraSchemaFactory = new HydraSchemaFactory($baseSchemaFactory);

$this->schemaFactory = new SchemaFactory($hydraSchemaFactory);
}

public function testBuildSchema(): void
{
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);

$this->assertTrue($resultSchema->isDefined());
$this->assertEquals(str_replace('\\', '.', Dummy::class).'.jsonhal', $resultSchema->getRootDefinitionKey());
}

public function testCustomFormatBuildSchema(): void
{
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json');

$this->assertTrue($resultSchema->isDefined());
$this->assertEquals(str_replace('\\', '.', Dummy::class), $resultSchema->getRootDefinitionKey());
}

public function testHasRootDefinitionKeyBuildSchema(): void
{
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class);
$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();

$this->assertArrayHasKey($rootDefinitionKey, $definitions);
$this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]);
$properties = $resultSchema['definitions'][$rootDefinitionKey]['properties'];
$this->assertArrayHasKey('_links', $properties);
$this->assertSame(
[
'type' => 'object',
'properties' => [
'self' => [
'type' => 'object',
'properties' => [
'href' => [
'type' => 'string',
'format' => 'iri-reference',
],
],
],
],
],
$properties['_links']
);
}

public function testSchemaTypeBuildSchema(): void
{
$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, OperationType::COLLECTION);
$definitionName = str_replace('\\', '.', Dummy::class).'.jsonhal';

$this->assertNull($resultSchema->getRootDefinitionKey());
$this->assertArrayHasKey('properties', $resultSchema);
$this->assertArrayHasKey('_embedded', $resultSchema['properties']);
$this->assertArrayHasKey('totalItems', $resultSchema['properties']);
$this->assertArrayHasKey('itemsPerPage', $resultSchema['properties']);
$this->assertArrayHasKey('_links', $resultSchema['properties']);
$properties = $resultSchema['definitions'][$definitionName]['properties'];
$this->assertArrayHasKey('_links', $properties);

$resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, null, null, null, null, true);

$this->assertNull($resultSchema->getRootDefinitionKey());
$this->assertArrayHasKey('properties', $resultSchema);
$this->assertArrayHasKey('_embedded', $resultSchema['properties']);
$this->assertArrayHasKey('totalItems', $resultSchema['properties']);
$this->assertArrayHasKey('itemsPerPage', $resultSchema['properties']);
$this->assertArrayHasKey('_links', $resultSchema['properties']);
$properties = $resultSchema['definitions'][$definitionName]['properties'];
$this->assertArrayHasKey('_links', $properties);
}
}
1 change: 0 additions & 1 deletion tests/Core/Hydra/JsonSchema/SchemaFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ public function testHasRootDefinitionKeyBuildSchema(): void
$definitions = $resultSchema->getDefinitions();
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();

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