diff --git a/src/Bridge/Symfony/Bundle/Controller/OperationController.php b/src/Bridge/Symfony/Bundle/Controller/OperationController.php index 9606431fd94665b0fa7d02be8b037544b09ae442..edc7c18b2824715f59d4411457cb29eda1766175 100644 --- a/src/Bridge/Symfony/Bundle/Controller/OperationController.php +++ b/src/Bridge/Symfony/Bundle/Controller/OperationController.php @@ -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); } diff --git a/src/Bridge/Symfony/Bundle/Controller/ResourceController.php b/src/Bridge/Symfony/Bundle/Controller/ResourceController.php index a6c808a3afa75ab55a2be76563d206830dacd1fb..fa055b136cda23ccfdf065984e52ac64ade3967f 100644 --- a/src/Bridge/Symfony/Bundle/Controller/ResourceController.php +++ b/src/Bridge/Symfony/Bundle/Controller/ResourceController.php @@ -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); } diff --git a/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php b/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php index b8f0250e63c35459b1e129bd893d3f9c9a469ad4..eb6fd360fce704c1785c1abae6deaccf6450d7e4 100644 --- a/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php +++ b/src/Bridge/Symfony/Serializer/ObjectMetadataNormalizer.php @@ -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]; } /** diff --git a/src/JSON/ClassMapping.php b/src/JSON/ClassMapping.php new file mode 100644 index 0000000000000000000000000000000000000000..a0bca2954047e769b1ee6f9f403e751814f7e898 --- /dev/null +++ b/src/JSON/ClassMapping.php @@ -0,0 +1,70 @@ +<?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; + } +} diff --git a/src/JSON/ClassMappingInterface.php b/src/JSON/ClassMappingInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6ef2c4ed2c3375fef57283426a53d33be795669e --- /dev/null +++ b/src/JSON/ClassMappingInterface.php @@ -0,0 +1,35 @@ +<?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); +} diff --git a/src/JSON/Document.php b/src/JSON/Document.php new file mode 100644 index 0000000000000000000000000000000000000000..e72d280740de200428825b7bbfe4d7565559699a --- /dev/null +++ b/src/JSON/Document.php @@ -0,0 +1,137 @@ +<?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; + } +} diff --git a/src/JSON/Pointer.php b/src/JSON/Pointer.php new file mode 100644 index 0000000000000000000000000000000000000000..adad6a4477823718a03980458ba86c35af4136be --- /dev/null +++ b/src/JSON/Pointer.php @@ -0,0 +1,158 @@ +<?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); + } +} diff --git a/tests/JSON/DocumentTest.php b/tests/JSON/DocumentTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a11bbf60f8a8ef947b60d8ba73a3932b8000093f --- /dev/null +++ b/tests/JSON/DocumentTest.php @@ -0,0 +1,85 @@ +<?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() + ); + } +} diff --git a/tests/JSON/PointerTest.php b/tests/JSON/PointerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..035bebab208e3da9f9bd8613a1dff0eadc571c2d --- /dev/null +++ b/tests/JSON/PointerTest.php @@ -0,0 +1,149 @@ +<?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'], + ]; + } +}