Skip to content
Merged
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 <dunglas@gmail.com>
*
* 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 <dunglas@gmail.com>
* @author Jachim Coudenys <jachimcoudenys@gmail.com>
*/
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 <dunglas@gmail.com>
*
* 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