Commit 8f628b4f authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Mise en place des classes dédiées à la gestion du JSON produit.

parent b03d22b7
No related merge requests found
Showing with 675 additions and 57 deletions
+675 -57
......@@ -20,6 +20,7 @@
namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\Controller;
use Irstea\ApiMetadata\Bridge\Symfony\Serializer\ObjectMetadataNormalizer;
use Irstea\ApiMetadata\Factory\Context;
use Irstea\ApiMetadata\Factory\Operation\OperationFactoryInterface;
use Irstea\ApiMetadata\Factory\Type\TypeFactoryInterface;
......@@ -82,7 +83,7 @@ class OperationController extends AbstractController
$ctx = new Context($this->typeFactory);
$metadata = $this->operationFactory->createOperation($operationId, $ctx);
$json = $this->serializer->serialize($metadata, 'json', ['root' => $metadata]);
$json = $this->serializer->serialize($metadata, 'json', [ObjectMetadataNormalizer::BASE_URI_CTX_KEY => $this->uriGenerator->generateURI($operationId)]);
return JsonResponse::fromJsonString($json);
}
......
......@@ -20,6 +20,7 @@
namespace Irstea\ApiMetadata\Bridge\Symfony\Bundle\Controller;
use Irstea\ApiMetadata\Bridge\Symfony\Serializer\ObjectMetadataNormalizer;
use Irstea\ApiMetadata\Factory\Context;
use Irstea\ApiMetadata\Helper\PropertyInfoType;
use Irstea\ApiMetadata\Model\Identity\ResourceIdentityInterface;
......@@ -51,7 +52,7 @@ class ResourceController extends AbstractController
$metadata = $metadata->getTarget();
}
$json = $this->serializer->serialize($metadata, 'json', ['root' => $metadata]);
$json = $this->serializer->serialize($metadata, 'json', [ObjectMetadataNormalizer::BASE_URI_CTX_KEY => $this->uriGenerator->generateURI($resourceId)]);
return JsonResponse::fromJsonString($json);
}
......
......@@ -21,11 +21,13 @@
namespace Irstea\ApiMetadata\Bridge\Symfony\Serializer;
use Assert\Assertion;
use Irstea\ApiMetadata\JSON\ClassMapping;
use Irstea\ApiMetadata\JSON\Document;
use Irstea\ApiMetadata\JSON\Pointer;
use Irstea\ApiMetadata\Model\ObjectMetadata;
use Irstea\ApiMetadata\Model\PropertyMetadata;
use Irstea\ApiMetadata\Model\ResourceMetadata;
use Irstea\ApiMetadata\URI\URIGeneratorInterface;
use Irstea\ApiMetadata\URI\Util;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
......@@ -35,12 +37,11 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
*/
class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
public const BASE_URI_CTX_KEY = 'baseURI';
public const BASE_URI_CTX_KEY = '$id';
private const URI_GENERATOR_CTX_KEY = __CLASS__ . 'uriGenerator';
private const URI_GENERATOR_CTX_KEY = __CLASS__ . '->uriGenerator';
private const REFERENCES_CTX_KEY = __CLASS__ . 'references';
private const DEFINITIONS_CTX_KEY = __CLASS__ . 'definitions';
private const CLASS_MAPPING_KEY = __CLASS__ . '->classMapping';
use NormalizerAwareTrait;
......@@ -72,16 +73,14 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
*/
public function normalize($object, $format = null, array $context = [])
{
Assertion::keyIsset($context, 'root');
/* @var ObjectMetadata $object */
Assertion::isInstanceOf($object, ObjectMetadata::class);
/** @var ObjectMetadata $object */
if ($object === $context['root']) {
Assertion::isInstanceOf($object, ResourceMetadata::class);
return $this->normalizeRoot($object, $format, $context);
if ($object instanceof ResourceMetadata && !isset($context[self::CLASS_MAPPING_KEY])) {
return (object) $this->normalizeRoot($object, $format, $context);
}
return $this->normalizeNonRoot($object, $format, $context);
return (object) $this->normalizeNonRoot($object, $format, $context);
}
/**
......@@ -95,36 +94,32 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
*/
private function normalizeRoot(ResourceMetadata $root, $format, array $context): array
{
$class = $root->getClass();
$rootURI = $context[self::BASE_URI_CTX_KEY] ?? $this->generateURI($root, $context);
$rootPointer = Pointer::from($rootURI);
if (isset($context[self::REFERENCES_CTX_KEY])) {
return ['$ref' => $context[self::REFERENCES_CTX_KEY][$class]];
}
$document = new Document($rootPointer);
$mapping = new ClassMapping($document, $rootPointer->join(Pointer::from('#definitions/')));
$context[self::DEFINITIONS_CTX_KEY] = $defs = new \ArrayObject();
$context[self::REFERENCES_CTX_KEY] = new \ArrayObject();
$context[self::CLASS_MAPPING_KEY] = $mapping;
$context[self::BASE_URI_CTX_KEY] = $rootURI;
$context[self::REFERENCES_CTX_KEY][$class] = $uri = $this->generateURI($root, $context, false);
$context[self::BASE_URI_CTX_KEY] = $uri;
$ptr = $mapping->resolve(
$root->getClass(),
function () use ($root, $format, $context) {
return $this->doNormalize($root, $format, $context);
}
);
$data = $this->doNormalize($root, $format, $context);
$document['#'] = $document[$ptr];
unset($document[$ptr]);
$operations = [];
foreach ($root->getOperations() as $operation) {
$operations[] = ['$ref' => $this->generateURI($operation, $context, true)];
$operations[] = (object) ['$ref' => $this->generateURI($operation, $context)];
}
$document['#operations'] = $operations;
return array_merge(
[
'$id' => $uri,
'$schema' => 'http://json-schema.org/schema#',
],
$data,
[
'operations' => $operations,
'definitions' => (object) ($defs->getArrayCopy()),
]
);
return $document->toArray();
}
/**
......@@ -134,19 +129,11 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
*
* @return string
*/
private function generateURI($resource, array $context, bool $relative): string
private function generateURI($resource, array $context): string
{
$uriGenerator = $context[self::URI_GENERATOR_CTX_KEY] ?? $this->uriGenerator;
$uri = $uriGenerator->generateURI($resource);
if ($relative) {
$base = $context[self::BASE_URI_CTX_KEY] ?? '';
if ($base) {
return Util::makeRelativeURI($base, $uri);
}
}
return $uri;
return $uriGenerator->generateURI($resource);
}
/**
......@@ -160,21 +147,16 @@ class ObjectMetadataNormalizer implements NormalizerAwareInterface, NormalizerIn
*/
private function normalizeNonRoot(ObjectMetadata $object, $format, array $context): array
{
$class = $object->getClass();
$mapping = $context[self::CLASS_MAPPING_KEY];
if (isset($context[self::REFERENCES_CTX_KEY][$class])) {
return ['$ref' => $context[self::REFERENCES_CTX_KEY][$class]];
}
$context[self::REFERENCES_CTX_KEY][$class] = $uri = $this->generateURI($object, $context, true);
if (strpos($uri, '#/definitions') === 0) {
$key = substr($uri, 14);
$context[self::DEFINITIONS_CTX_KEY][$key] = $this->doNormalize($object, $format, $context);
}
$pointer = $mapping->resolve(
$object->getClass(),
function () use ($object, $format, $context) {
return $this->doNormalize($object, $format, $context);
}
);
return ['$ref' => $uri];
return ['$ref' => $pointer];
}
/**
......
<?php declare(strict_types=1);
/*
* This file is part of "irstea/api-metadata".
*
* Copyright (C) 2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\ApiMetadata\JSON;
/**
* Class ClassMapping.
*/
class ClassMapping implements ClassMappingInterface
{
/**
* @var Document
*/
private $document;
/**
* @var Pointer
*/
private $base;
/**
* @var array<string, Pointer>
*/
private $pointers = [];
/**
* ClassMapping constructor.
*
* @param Document $document
* @param Pointer $base
*/
public function __construct(Document $document, Pointer $base)
{
$this->document = $document;
$this->base = $base;
}
/**
* {@inheritdoc}
*/
public function resolve(string $className, callable $resolver)
{
if (isset($this->pointers[$className])) {
return $this->pointers[$className];
}
$pointer = $this->base->join(Pointer::from('#' . $className));
$this->pointers[$className] = $pointer;
$this->document[$pointer] = $resolver($className);
return $pointer;
}
}
<?php declare(strict_types=1);
/*
* This file is part of "irstea/api-metadata".
*
* Copyright (C) 2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\ApiMetadata\JSON;
/**
* Interface ClassMappingInterface.
*/
interface ClassMappingInterface
{
/**
* @param string $className
* @param callable $resolver
*
* @return mixed
*/
public function resolve(string $className, callable $resolver);
}
<?php declare(strict_types=1);
/*
* This file is part of "irstea/api-metadata".
*
* Copyright (C) 2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\ApiMetadata\JSON;
/**
* Class Document.
*/
class Document implements \ArrayAccess
{
/** @var Pointer */
private $root;
/**
* @var array<string, mixed>
*/
private $data;
/**
* Document constructor.
*
* @param Pointer $root
*/
public function __construct(Pointer $root)
{
$this->root = $root->getDocumentRoot();
}
/**
* {@inheritdoc}
*/
public function offsetExists($offset): bool
{
$pointer = Pointer::from($offset);
return $pointer->inSameDocumentAs($this->root) && \array_key_exists($pointer->getFragment(), $this->data);
}
/**
* {@inheritdoc}
*/
public function offsetGet($offset)
{
$pointer = Pointer::from($offset);
if (!$pointer->inSameDocumentAs($this->root)) {
return null;
}
return $this->data[$pointer->getFragment()] ?? null;
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value): void
{
$pointer = Pointer::from($offset);
if (!$pointer->inSameDocumentAs($this->root)) {
return;
}
$this->data[$pointer->getFragment()] = $value;
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset): void
{
$pointer = Pointer::from($offset);
if (!$pointer->inSameDocumentAs($this->root)) {
return;
}
unset($this->data[$pointer->getFragment()]);
}
/**
* @return array
*/
public function toArray(): array
{
$parts = [['$id' => $this->root->toString()]];
$paths = array_keys($this->data);
usort($paths, [self::class, 'comparePaths']);
foreach ($paths as $path) {
if (!$path) {
$parts[] = $this->data[$path];
continue;
}
$root = [];
$current = &$root;
foreach (explode('/', $path) as $next) {
if (!isset($current[$next])) {
$current[$next] = [];
}
$current = &$current[$next];
}
$current = $this->data[$path];
$parts[] = $root;
}
return \array_replace_recursive(...$parts);
}
/**
* @param string $a
* @param string $b
*
* @return bool
*/
public static function comparePaths(string $a, string $b): bool
{
$aParts = explode('/', $a);
$bParts = explode('/', $b);
return $aParts > $bParts;
}
}
<?php declare(strict_types=1);
/*
* This file is part of "irstea/api-metadata".
*
* Copyright (C) 2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\ApiMetadata\JSON;
use function Sabre\Uri\build;
use function Sabre\Uri\normalize;
use function Sabre\Uri\parse;
/**
* Class Pointer.
*/
final class Pointer
{
/**
* @var string
*/
private $fragment;
/**
* @var string
*/
private $url;
/**
* Pointer constructor.
*
* @param string $url
* @param string $fragment
*/
private function __construct(string $url, string $fragment)
{
$this->url = $url;
$this->fragment = $fragment;
}
/**
* Get fragment.
*
* @return string
*/
public function getFragment(): string
{
return $this->fragment;
}
/**
* Get url.
*
* @return string
*/
public function getURL(): string
{
return $this->url;
}
/**
* @param Pointer $other
*
* @return bool
*/
public function isEqualTo(Pointer $other): bool
{
return $this->inSameDocumentAs($other) && $this->fragment === $other->fragment;
}
/**
* @param Pointer $other
*
* @return bool
*/
public function inSameDocumentAs(Pointer $other): bool
{
return !$other->url || !$this->url || $other->url === $this->url;
}
/**
* @return Pointer
*/
public function getDocumentRoot(): Pointer
{
return $this->fragment ? new self($this->url, '') : $this;
}
/**
* @return string
*/
public function toString(): string
{
return $this->url . '#' . $this->fragment;
}
/**
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
/**
* @param Pointer $other
*
* @return Pointer
*/
public function join(Pointer $other): Pointer
{
if (!$other->inSameDocumentAs($this)) {
return $other;
}
$thisFragment = $this->fragment;
$i = strrpos($thisFragment, '/');
$otherFragment = $other->fragment;
if ($i === false || strpos($otherFragment, '/') === 0) {
return new Pointer($this->url, $otherFragment);
}
return new Pointer($this->url, substr($thisFragment, 0, $i + 1) . $otherFragment);
}
/**
* @param string|Pointer $uri
*
* @return Pointer
*/
public static function from($uri): Pointer
{
if ($uri instanceof self) {
return $uri;
}
$data = parse(normalize($uri));
$fragment = $data['fragment'] ?: '';
unset($data['fragment']);
$url = build($data);
return new self($url, $fragment);
}
}
<?php declare(strict_types=1);
/*
* This file is part of "irstea/api-metadata".
*
* Copyright (C) 2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\ApiMetadata\Tests\JSON;
use Irstea\ApiMetadata\JSON\Document;
use Irstea\ApiMetadata\JSON\Pointer;
use PHPUnit\Framework\TestCase;
/**
* Class DocumentTest.
*
* @covers \Irstea\ApiMetadata\JSON\Pointer
* @covers \Irstea\ApiMetadata\JSON\Document
*/
class DocumentTest extends TestCase
{
public function testArrayAccess()
{
$root = Pointer::from('document#root');
$doc = new Document($root);
$doc['document#a/0/k'] = 5;
$doc['document#a/1/k'] = 8;
$doc['document#b'] = 'truc';
$doc['document#a/5/k'] = 7;
$doc['document#'] = ['foo' => 'bar'];
unset($doc['document#a/5/k']);
unset($doc['external#a/5/k']);
self::assertTrue(isset($doc['document#a/0/k']));
self::assertEquals(5, $doc['document#a/0/k']);
self::assertFalse(isset($doc['document#a/5/k']));
self::assertFalse(isset($doc['external#a/0/k']));
self::assertNull($doc['external#a/0/k']);
}
public function testToArray()
{
$root = Pointer::from('document#root');
$doc = new Document($root);
$doc['document#a'] = [2 => 7];
$doc['document#a/0/k'] = 5;
$doc['document#a/1/k'] = 8;
$doc['document#b'] = 'truc';
$doc['document#'] = ['foo' => 'bar'];
self::assertEquals(
[
'$id' => $root->getDocumentRoot()->toString(),
'foo' => 'bar',
'a' => [
['k' => 5],
['k' => 8],
7,
],
'b' => 'truc',
],
$doc->toArray()
);
}
}
<?php declare(strict_types=1);
/*
* This file is part of "irstea/api-metadata".
*
* Copyright (C) 2019 IRSTEA
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the GNU
* Lesser General Public License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
namespace Irstea\ApiMetadata\Tests\JSON;
use Irstea\ApiMetadata\JSON\Pointer;
use PHPUnit\Framework\TestCase;
/**
* Class Test.
*
* @covers \Irstea\ApiMetadata\JSON\Pointer
*/
class PointerTest extends TestCase
{
/**
* @param string $expected
* @param string $uri
*
* @dataProvider getFromTestCases
*/
public function testFrom(string $expected, string $uri): void
{
$pointer = Pointer::from($uri);
self::assertEquals($expected, $pointer->toString());
}
/**
* @return array
*/
public function getFromTestCases(): array
{
return [
['http://example.com/#', 'http://example.com'],
['http://example.com/#/', 'http://example.com/#/'],
['http://example.com/#document', 'http://example.com#document'],
['http://example.com/#/document', 'http://example.com#/document'],
['http://example.com/#document/', 'http://example.com#document/'],
['http://example.com/#/document/', 'http://example.com#/document/'],
];
}
/**
* @param string $expected
* @param string $uri
*
* @dataProvider getGetDocumentRootTestCases
*/
public function testGetDocumentRoot(string $expected, string $uri): void
{
$pointer = Pointer::from($uri);
self::assertEquals($expected, $pointer->getDocumentRoot()->toString());
}
/**
* @return array
*/
public function getGetDocumentRootTestCases(): array
{
return [
['http://example.com/#', 'http://example.com'],
['http://example.com/#', 'http://example.com/#/'],
['http://example.com/#', 'http://example.com#document'],
['http://example.com/#', 'http://example.com#/document'],
['http://example.com/#', 'http://example.com#document/'],
['http://example.com/#', 'http://example.com#/document/bla/'],
];
}
/**
* @param bool $expected
* @param string $uri
*
* @dataProvider getInSameDocumentAsTestCases
*/
public function testinSameDocumentAs(bool $expected, string $a, string $b): void
{
$pa = Pointer::from($a);
$pb = Pointer::from($b);
if ($expected) {
self::assertTrue($pa->inSameDocumentAs($pb));
self::assertTrue($pb->inSameDocumentAs($pa));
} else {
self::assertFalse($pa->inSameDocumentAs($pb));
self::assertFalse($pb->inSameDocumentAs($pa));
}
}
/**
* @return array
*/
public function getInSameDocumentAsTestCases(): array
{
return [
[true, 'http://example.com/#', 'http://example.com'],
[true, 'http://example.com/#/', 'http://example.com/#/'],
[true, 'http://example.com/#document/bla', 'http://example.com#document'],
[false, 'http://example.org/#', 'http://example.com#'],
[false, 'http://example.org/#', 'http://example.com#document/'],
];
}
/**
* @param string $expected
* @param string $base
* @param string $other
*
* @dataProvider getJoinTestCases
*/
public function testJoin(string $expected, string $base, string $other): void
{
$basePointer = Pointer::from($base);
$otherPointer = Pointer::from($other);
$result = $basePointer->join($otherPointer);
self::assertEquals($expected, $result->toString());
}
/**
* @return array
*/
public function getJoinTestCases(): array
{
return [
['http://example.com/#/documents', 'http://example.com', '#/documents'],
['http://example.com/#/documents', 'http://example.com#/bla', '#/documents'],
['http://example.com/#/documents', 'http://example.com#/bla', '#documents'],
['http://example.com/#/bla/documents', 'http://example.com#/bla/', '#documents'],
];
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment