Skip to content

Commit c031e85

Browse files
committed
Add initial version
1 parent 9dcffdd commit c031e85

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed

src/InvalidArgumentException.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Slavcodev\JsonPointer;
6+
7+
use InvalidArgumentException as PhpInvalidArgumentException;
8+
use Throwable;
9+
10+
final class InvalidArgumentException extends PhpInvalidArgumentException implements JsonPointerException
11+
{
12+
public function __construct(string $message, Throwable $previous = null)
13+
{
14+
parent::__construct($message, 0, $previous);
15+
}
16+
}

src/JsonPointer.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Slavcodev\JsonPointer;
6+
7+
use function explode;
8+
use function mb_substr;
9+
use function preg_match;
10+
use function strtr;
11+
12+
/**
13+
* The object implementation of the JSON pointer
14+
*
15+
* Rules:
16+
*
17+
* - A JSON Pointer is a Unicode string (see [RFC4627], Section 3) containing a sequence of zero or more reference tokens,
18+
* each prefixed by a '/' (%x2F) character;
19+
* - Each reference token contains zero or more Unicode characters: %x00-10FFFF (except special chars `%x2F` and `%x7E`);
20+
* - Characters `~` and `/` are encoded in reference token into `~0` and `~1` respectively;
21+
*
22+
* @see https://tools.ietf.org/html/rfc6901
23+
*
24+
* @psalm-immutable
25+
*/
26+
final class JsonPointer
27+
{
28+
private const SPECIAL_CASES = ['~1' => '/', '~0' => '~'];
29+
30+
public string $value;
31+
32+
public bool $anchored = false;
33+
34+
public array $tokens = [];
35+
36+
/**
37+
* @throws InvalidArgumentException if passed string is invalid JSON pointer.
38+
*/
39+
public function __construct(string $value = '')
40+
{
41+
$this->value = $value;
42+
43+
// If URI fragment representation, strip `#`.
44+
if (!empty($value) && $value[0] === '#') {
45+
$value = mb_substr($value, 1);
46+
$this->anchored = true;
47+
}
48+
49+
if (!empty($value)) {
50+
if ($value[0] !== '/') {
51+
throw new InvalidArgumentException('Invalid JSON pointer syntax');
52+
}
53+
54+
foreach (explode('/', mb_substr($value, 1)) as $key) {
55+
if (preg_match('/~[^0-1]/', $key)) {
56+
throw new InvalidArgumentException('Invalid JSON pointer syntax');
57+
}
58+
59+
$this->tokens[] = strtr($key, self::SPECIAL_CASES);
60+
}
61+
}
62+
}
63+
64+
public function __toString(): string
65+
{
66+
return $this->toString();
67+
}
68+
69+
public function toString(): string
70+
{
71+
return $this->value;
72+
}
73+
}

src/JsonPointerException.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Slavcodev\JsonPointer;
6+
7+
interface JsonPointerException
8+
{
9+
}

tests/JsonPointerTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Slavcodev\JsonPointer\Tests;
6+
7+
use Slavcodev\JsonPointer\InvalidArgumentException;
8+
use Slavcodev\JsonPointer\JsonPointer;
9+
use function array_shift;
10+
11+
final class JsonPointerTest extends TestCase
12+
{
13+
/**
14+
* @test
15+
* @dataProvider provideInvalidJsonPointers
16+
*/
17+
public function validatesValueOnConstruct(string $value): void
18+
{
19+
$this->expectExceptionObject(new InvalidArgumentException('Invalid JSON pointer syntax'));
20+
new JsonPointer($value);
21+
}
22+
23+
public function provideInvalidJsonPointers(): iterable
24+
{
25+
return [
26+
'must start with token prefix' => ['foo'],
27+
'must start with `#` representing URI fragment' => ['#foo'],
28+
'must start with token prefix or `#`' => [' /'],
29+
'must not contain unescaped `~`' => ['/~foo'],
30+
];
31+
}
32+
33+
/**
34+
* @test
35+
* @dataProvider provideJsonPointersAndUriFragments
36+
*/
37+
public function isInstantiatedFromString(string $value, array $expectedTokens, bool $anchored): void
38+
{
39+
$pointer = new JsonPointer($value);
40+
self::assertSame($value, $pointer->toString());
41+
self::assertSame($value, (string) $pointer);
42+
self::assertSame($expectedTokens, $pointer->tokens);
43+
self::assertSame($anchored, $pointer->anchored);
44+
}
45+
46+
/**
47+
* @test
48+
*/
49+
public function isInstantiatedWithDefaultValue(): void
50+
{
51+
$pointer = new JsonPointer();
52+
self::assertSame('', $pointer->toString());
53+
self::assertSame('', (string) $pointer);
54+
self::assertSame([], $pointer->tokens);
55+
self::assertFalse($pointer->anchored);
56+
}
57+
58+
public function provideJsonPointers(): iterable
59+
{
60+
return [
61+
'basic' => ['/foo', ['foo']],
62+
'basic (1)' => ['/foo/bar', ['foo', 'bar']],
63+
'empty' => ['', []],
64+
'empty token' => ['/', ['']],
65+
'empty tokens' => ['//', ['', '']],
66+
'token with space' => ['/ ', [' ']],
67+
'token with `%`' => ['/f%o', ['f%o']],
68+
'token with `^`' => ['/f^o', ['f^o']],
69+
'token with `|`' => ['/f|o', ['f|o']],
70+
'token with `\\`' => ['/f\\o', ['f\\o']],
71+
'token with `\'`' => ['/f\'o', ['f\'o']],
72+
'token with NUL (Unicode U+0000)' => ["/f\0o", ["f\0o"]],
73+
'token with `"`' => ['/f"o', ['f"o']],
74+
'token with `/`' => ['/~1foo/bar~1/baz', ['/foo', 'bar/', 'baz']],
75+
'token with `/` (1)' => ['/f~1o', ['f/o']],
76+
'token with `~`' => ['/~0foo/bar~0/baz', ['~foo', 'bar~', 'baz']],
77+
'token with `~` (1)' => ['/f~0o', ['f~o']],
78+
'numeric token' => ['/foo/0', ['foo', '0']],
79+
];
80+
}
81+
82+
public function provideJsonPointersAndUriFragments(): iterable
83+
{
84+
foreach ($this->provideJsonPointers() as $key => $set) {
85+
$value = array_shift($set);
86+
yield "{$key} - {$value}" => [$value, ...$set, false];
87+
yield "{$key} - #{$value}" => ["#{$value}", ...$set, true];
88+
}
89+
}
90+
}

tests/TestCase.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Slavcodev\JsonPointer\Tests;
6+
7+
use PHPUnit\Framework;
8+
9+
abstract class TestCase extends Framework\TestCase
10+
{
11+
}

0 commit comments

Comments
 (0)