diff --git a/CHANGELOG.md b/CHANGELOG.md index 73946d67..64ccf935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), For a full diff see [`a5ba52c...main`][a5ba52c...main]. +### Added + +- Added `ReferenceToken` as a value object ([#1]), by [@localheinz] + [a5ba52c...main]: https://github.com/ergebnis/json-pointer/compare/a5ba52c...main + +[#1]: https://github.com/ergebnis/json-pointer/pull/1 + +[@localheinz]: https://github.com/localheinz diff --git a/README.md b/README.md index 5d241da8..86875e5b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,67 @@ composer require ergebnis/json-pointer ## Usage -:bulb: This is a great place for showing a few usage examples! +### ReferenceToken + +You can create a `ReferenceToken` from an unescaped `string` value: + +```php +toEscapedString(); // 'foo~1bar' +$referenceToken->toUnescapedString(); // 'foo/bar' +``` + +You can create a `ReferenceToken` from an escaped `string` value: + +```php +toEscapedString(); // 'foo~1bar' +$referenceToken->toUnescapedString(); // 'foo/bar' +``` + +You can create a `ReferenceToken` from an `int` value: + +```php +toEscapedString(); // '9001' +$referenceToken->toUnescapedString(); // '9001' +``` + +You can compare `ReferenceToken`s: + +```php +equals($two); // true +``` ## Changelog diff --git a/composer.json b/composer.json index 26b3c068..3c224868 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "type": "library", "keywords": [ "json", - "pointer" + "pointer", + "rfc6901" ], "authors": [ { diff --git a/src/Example.php b/src/Example.php deleted file mode 100644 index 8945ecf5..00000000 --- a/src/Example.php +++ /dev/null @@ -1,34 +0,0 @@ -name = $name; - } - - public static function fromName(string $name): self - { - return new self($name); - } - - public function name(): string - { - return $this->name; - } -} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php new file mode 100644 index 00000000..9e7ab99c --- /dev/null +++ b/src/Exception/Exception.php @@ -0,0 +1,18 @@ +escapedValue = $escapedValue; + } + + /** + * @throws Exception\InvalidReferenceToken + */ + public static function fromInt(int $value): self + { + if (0 > $value) { + throw Exception\InvalidReferenceToken::fromInt($value); + } + + return new self((string) $value); + } + + /** + * @throws Exception\InvalidReferenceToken + */ + public static function fromEscapedString(string $value): self + { + if (1 !== \preg_match('/^(?P((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[01]))*)$/u', $value)) { + throw Exception\InvalidReferenceToken::fromString($value); + } + + return new self($value); + } + + public static function fromUnescapedString(string $value): self + { + return self::fromEscapedString(\str_replace( + [ + '~', + '/', + ], + [ + '~0', + '~1', + ], + $value, + )); + } + + public function toEscapedString(): string + { + return $this->escapedValue; + } + + public function toUnescapedString(): string + { + return \str_replace( + [ + '~1', + '~0', + ], + [ + '/', + '~', + ], + $this->escapedValue, + ); + } + + public function equals(self $other): bool + { + return $this->escapedValue === $other->escapedValue; + } +} diff --git a/test/Unit/ExampleTest.php b/test/Unit/ExampleTest.php deleted file mode 100644 index 93a3f658..00000000 --- a/test/Unit/ExampleTest.php +++ /dev/null @@ -1,37 +0,0 @@ -sentence; - - $example = Example::fromName($name); - - self::assertSame($name, $example->name()); - } -} diff --git a/test/Unit/Exception/InvalidReferenceTokenTest.php b/test/Unit/Exception/InvalidReferenceTokenTest.php new file mode 100644 index 00000000..746c3f28 --- /dev/null +++ b/test/Unit/Exception/InvalidReferenceTokenTest.php @@ -0,0 +1,56 @@ +word(); + + $exception = Exception\InvalidReferenceToken::fromString($value); + + $message = \sprintf( + 'Value "%s" does not appear to be a valid JSON Pointer reference token.', + $value, + ); + + self::assertSame($message, $exception->getMessage()); + } + + public function testFromIntReturnsInvalidReferenceToken(): void + { + $value = self::faker()->numberBetween(); + + $exception = Exception\InvalidReferenceToken::fromInt($value); + + $message = \sprintf( + 'Value "%d" does not appear to be a valid JSON Pointer array index.', + $value, + ); + + self::assertSame($message, $exception->getMessage()); + } +} diff --git a/test/Unit/ReferenceTokenTest.php b/test/Unit/ReferenceTokenTest.php new file mode 100644 index 00000000..8e6f08eb --- /dev/null +++ b/test/Unit/ReferenceTokenTest.php @@ -0,0 +1,242 @@ +expectException(Exception\InvalidReferenceToken::class); + + ReferenceToken::fromEscapedString($value); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 + * + * @return \Generator + */ + public function provideInvalidEscapedStringValue(): \Generator + { + $values = [ + 'property-with-unescaped-forward-slash' => 'foo/bar', + 'property-with-unescaped-tilde' => 'foo~bar', + ]; + + foreach ($values as $key => $value) { + yield $key => [ + $value, + ]; + } + } + + /** + * @dataProvider provideUnescapedAndEscapedValue + */ + public function testFromEscapedStringReturnsReferenceToken( + string $unescaped, + string $escaped + ): void { + $referenceToken = ReferenceToken::fromEscapedString($escaped); + + self::assertSame($escaped, $referenceToken->toEscapedString()); + self::assertSame($unescaped, $referenceToken->toUnescapedString()); + } + + /** + * @dataProvider provideUnescapedAndEscapedValue + */ + public function testFromUnescapedStringReturnsReferenceToken( + string $unescaped, + string $escaped + ): void { + $referenceToken = ReferenceToken::fromUnescapedString($unescaped); + + self::assertSame($escaped, $referenceToken->toEscapedString()); + self::assertSame($unescaped, $referenceToken->toUnescapedString()); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 + * + * @return \Generator + */ + public function provideUnescapedAndEscapedValue(): \Generator + { + $values = [ + 'integerish-9000' => [ + '9000', + '9000', + ], + 'integerish-zero' => [ + '0', + '0', + ], + 'string-back-slash' => [ + '\\', + '\\', + ], + 'string-caret' => [ + '^', + '^', + ], + 'string-percent' => [ + '%', + '%', + ], + 'string-pipe' => [ + '|', + '|', + ], + 'string-quote-double' => [ + '"', + '"', + ], + 'string-quote-single' => [ + "'", + "'", + ], + 'string-slash-backward' => [ + '\\', + '\\', + ], + 'string-slash-forward' => [ + '/', + '~1', + ], + 'string-space' => [ + ' ', + ' ', + ], + 'string-word' => [ + 'foo', + 'foo', + ], + 'string-tilde' => [ + '~', + '~0', + ], + 'string-unicode-character' => [ + '😆', + '😆', + ], + 'string-with-caret' => [ + 'foo^bar', + 'foo^bar', + ], + 'string-with-percent' => [ + 'foo%bar', + 'foo%bar', + ], + 'string-with-pipe' => [ + 'foo|bar', + 'foo|bar', + ], + 'string-with-quote-double' => [ + 'foo"bar', + 'foo"bar', + ], + 'string-with-quote-single' => [ + "foo'bar", + "foo'bar", + ], + 'string-with-slash-backward' => [ + 'foo\\bar', + 'foo\\bar', + ], + 'string-with-slash-forward' => [ + 'foo/bar', + 'foo~1bar', + ], + 'string-with-space' => [ + 'foo bar', + 'foo bar', + ], + 'string-with-tilde' => [ + 'foo~bar', + 'foo~0bar', + ], + 'string-with-unicode-character' => [ + 'foo😆bar', + 'foo😆bar', + ], + ]; + + foreach ($values as $key => [$unescaped, $escaped]) { + yield $key => [ + $unescaped, + $escaped, + ]; + } + } + + /** + * @dataProvider \Ergebnis\DataProvider\IntProvider::lessThanZero() + */ + public function testFromIntRejectsInvalidValue(int $value): void + { + $this->expectException(Exception\InvalidReferenceToken::class); + + ReferenceToken::fromInt($value); + } + + /** + * @dataProvider \Ergebnis\DataProvider\IntProvider::greaterThanZero() + * @dataProvider \Ergebnis\DataProvider\IntProvider::zero() + */ + public function testFromIntReturnsReferenceToken(int $value): void + { + $referenceToken = ReferenceToken::fromInt($value); + + self::assertSame((string) $value, $referenceToken->toEscapedString()); + self::assertSame((string) $value, $referenceToken->toUnescapedString()); + } + + public function testEqualsReturnsFalseWhenEscapedValueIsDifferent(): void + { + $faker = self::faker(); + + $one = ReferenceToken::fromUnescapedString($faker->sentence()); + $two = ReferenceToken::fromUnescapedString($faker->sentence()); + + self::assertFalse($one->equals($two)); + } + + public function testEqualsReturnsTrueWhenEscapedValueIsSame(): void + { + $value = self::faker()->sentence(); + + $one = ReferenceToken::fromUnescapedString($value); + $two = ReferenceToken::fromUnescapedString($value); + + self::assertTrue($one->equals($two)); + } +}