diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ccf935..614de4bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased -For a full diff see [`a5ba52c...main`][a5ba52c...main]. +For a full diff see [`1.0.0...main`][1.0.0...main]. + +## [`1.0.0`][1.0.0] + +For a full diff see [`a5ba52c...1.0.0`][a5ba52c...1.0.0]. ### Added - Added `ReferenceToken` as a value object ([#1]), by [@localheinz] +- Added `JsonPointer` as a value object ([#2]), by [@localheinz] -[a5ba52c...main]: https://github.com/ergebnis/json-pointer/compare/a5ba52c...main +[a5ba52c...1.0.0]: https://github.com/ergebnis/json-pointer/compare/a5ba52c...1.0.0 +[1.0.0...main]: https://github.com/ergebnis/json-pointer/compare/1.0.0...main [#1]: https://github.com/ergebnis/json-pointer/pull/1 +[#2]: https://github.com/ergebnis/json-pointer/pull/2 [@localheinz]: https://github.com/localheinz diff --git a/README.md b/README.md index 56ba05d3..1c02a4f2 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,69 @@ $two = Pointer\ReferenceToken::fromEscapedString('foo~1bar'); $one->equals($two); // true ``` +### `JsonPointer` + +You can create a `JsonPointer` referencing a document: + +```php +toString(); // '' +``` + +You can create a `JsonPointer` from a `string` value: + +```php +toString(); // '/foo/bar' +``` + +You can compare `JsonPointer`s: + +```php +equals($two); // false +``` + +You can append a `ReferenceToken` to a `JsonPointer`: + +```php +append($referenceToken); + +$newJsonPointer->toString(); // '/foo/bar/baz' +``` + ## Changelog Please have a look at [`CHANGELOG.md`](CHANGELOG.md). diff --git a/src/Exception/InvalidJsonPointer.php b/src/Exception/InvalidJsonPointer.php new file mode 100644 index 00000000..7320e8c3 --- /dev/null +++ b/src/Exception/InvalidJsonPointer.php @@ -0,0 +1,25 @@ +value = $value; + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-3 + * + * @throws Exception\InvalidJsonPointer + */ + public static function fromString(string $value): self + { + if (1 !== \preg_match('/^(\/(?P((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[01]))*))*$/u', $value, $matches)) { + throw Exception\InvalidJsonPointer::fromString($value); + } + + return new self($value); + } + + public static function document(): self + { + return new self(''); + } + + public function append(ReferenceToken $referenceToken): self + { + return new self(\sprintf( + '%s/%s', + $this->value, + $referenceToken->toEscapedString(), + )); + } + + public function toString(): string + { + return $this->value; + } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } +} diff --git a/test/Unit/Exception/InvalidJsonPointerTest.php b/test/Unit/Exception/InvalidJsonPointerTest.php new file mode 100644 index 00000000..2cbc18eb --- /dev/null +++ b/test/Unit/Exception/InvalidJsonPointerTest.php @@ -0,0 +1,42 @@ +sentence(); + + $exception = Exception\InvalidJsonPointer::fromString($value); + + $message = \sprintf( + 'Value "%s" does not appear to be a valid JSON Pointer.', + $value, + ); + + self::assertSame($message, $exception->getMessage()); + } +} diff --git a/test/Unit/JsonPointerTest.php b/test/Unit/JsonPointerTest.php new file mode 100644 index 00000000..6cede958 --- /dev/null +++ b/test/Unit/JsonPointerTest.php @@ -0,0 +1,187 @@ +expectException(Exception\InvalidJsonPointer::class); + + JsonPointer::fromString($value); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 + * + * @return \Generator + */ + public function provideInvalidValue(): \Generator + { + $values = [ + 'does-not-start-with-forward-slash' => 'foo', + 'property-with-unescaped-tilde' => '/foo~bar', + 'property-with-unescaped-tildes' => '/foo~~bar', + ]; + + foreach ($values as $key => $value) { + yield $key => [ + $value, + ]; + } + } + + /** + * @dataProvider provideValidValue + */ + public function testFromStringReturnsJsonPointer(string $value): void + { + $jsonPointer = JsonPointer::fromString($value); + + self::assertSame($value, $jsonPointer->toString()); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 + * + * @return \Generator + */ + public function provideValidValue(): \Generator + { + $values = [ + 'document' => '', + 'document-root' => '/', + 'property-points-to-array-element' => '/foo/0', + 'property-with-escaped-forward-slash' => '/a~1b', + 'property-with-escaped-tilde' => '/m~0n', + 'property-with-unescaped-back-slash' => '/i\\j', + 'property-with-unescaped-caret' => '/e^f', + 'property-with-unescaped-double-quote' => '/k"l', + 'property-with-unescaped-percent' => '/c%d', + 'property-pipe' => '/|', + 'property-with-pipe' => '/foo|bar', + 'property-quote-single' => "/foo'bar", + 'property-quote-double' => '/foo"bar', + 'property-space' => '/ ', + 'property-text' => '/foo', + 'property-unicode-character' => '/😆', + ]; + + foreach ($values as $key => $value) { + yield $key => [ + $value, + ]; + } + } + + public function testDocumentReturnsJsonPointer(): void + { + $jsonPointer = JsonPointer::document(); + + self::assertSame('', $jsonPointer->toString()); + } + + /** + * @dataProvider provideExpectedJsonPointerJsonPointerAndSegment + */ + public function testAppendReturnsJsonPointer( + JsonPointer $jsonPointer, + ReferenceToken $referenceToken, + JsonPointer $expectedJsonPointer + ): void { + $mutated = $jsonPointer->append($referenceToken); + + self::assertNotSame($jsonPointer, $mutated); + self::assertEquals($expectedJsonPointer, $mutated); + } + + /** + * @return \Generator + */ + public function provideExpectedJsonPointerJsonPointerAndSegment(): \Generator + { + $values = [ + 'document-and-reference-token-from-unescaped-string' => [ + JsonPointer::document(), + ReferenceToken::fromUnescapedString('foo'), + JsonPointer::fromString('/foo'), + ], + 'document-from-string-and-reference-token-from-unescaped-string' => [ + JsonPointer::fromString(''), + ReferenceToken::fromUnescapedString('foo'), + JsonPointer::fromString('/foo'), + ], + 'pointer-and-reference-token-from-int' => [ + JsonPointer::fromString('/foo'), + ReferenceToken::fromInt(9000), + JsonPointer::fromString('/foo/9000'), + ], + 'pointer-and-reference-token-from-unescaped-string' => [ + JsonPointer::fromString('/foo'), + ReferenceToken::fromUnescapedString('bar/baz'), + JsonPointer::fromString('/foo/bar~1baz'), + ], + 'pointer-and-reference-token-from-escaped-string' => [ + JsonPointer::fromString('/foo'), + ReferenceToken::fromEscapedString('bar~1baz'), + JsonPointer::fromString('/foo/bar~1baz'), + ], + ]; + + foreach ($values as $key => [$expectedJsonPointer, $jsonPointer, $segment]) { + yield $key => [ + $expectedJsonPointer, + $jsonPointer, + $segment, + ]; + } + } + + public function testEqualsReturnsFalseWhenValueIsDifferent(): void + { + $one = JsonPointer::fromString('/foo/bar/0/baz~0'); + $two = JsonPointer::fromString('/foo/bar/1'); + + self::assertFalse($one->equals($two)); + } + + public function testEqualsReturnsFalseWhenValueIsSame(): void + { + $value = '/foo/bar/0/baz~0'; + + $one = JsonPointer::fromString($value); + $two = JsonPointer::fromString($value); + + self::assertTrue($one->equals($two)); + } +}