Skip to content

Commit a880361

Browse files
authored
Merge pull request #6 from ergebnis/feature/uri-fragment-identifier
Enhancement: Add support for URI fragment identifiers
2 parents d545054 + 7caebfa commit a880361

11 files changed

+423
-39
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ For a full diff see [`1.0.0...2.0.0`][1.0.0...2.0.0].
1414

1515
## Added
1616

17+
- Added named constructors `JsonPointer::fromUriFragmentIdentifierString()` and `ReferenceToken::fromUriFragmentIdentifierString()` to allow creation from URI fragment identifier representations ([#6]), by [@localheinz]
1718
- Added named constructor `JsonPointer::fromReferenceTokens()` to allow creation of `JsonPointer` from `ReferenceToken`s ([#9]), by [@localheinz]
1819

1920
## Changed
@@ -45,6 +46,7 @@ For a full diff see [`a5ba52c...1.0.0`][a5ba52c...1.0.0].
4546
[#2]: https://github.com/ergebnis/json-pointer/pull/2
4647
[#4]: https://github.com/ergebnis/json-pointer/pull/4
4748
[#5]: https://github.com/ergebnis/json-pointer/pull/5
49+
[#6]: https://github.com/ergebnis/json-pointer/pull/6
4850
[#9]: https://github.com/ergebnis/json-pointer/pull/9
4951

5052
[@localheinz]: https://github.com/localheinz

README.md

+52-13
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ declare(strict_types=1);
3434

3535
use Ergebnis\Json\Pointer;
3636

37-
$referenceToken = Pointer\ReferenceToken::fromString('foo/bar');
37+
$referenceToken = Pointer\ReferenceToken::fromString('foo/9000/😆');
3838

39-
$referenceToken->toJsonString(); // 'foo~1bar'
40-
$referenceToken->toString(); // 'foo/bar'
39+
$referenceToken->toJsonString(); // 'foo~19000~😆'
40+
$referenceToken->toString(); // 'foo/9000/😆'
41+
$referenceToken->toUriFragmentIdentifierString(); // 'foo~19000~1%F0%9F%98%86'
4142
```
4243

4344
You can create a `ReferenceToken` from a [JSON `string` value](https://datatracker.ietf.org/doc/html/rfc6901#section-5):
@@ -49,10 +50,27 @@ declare(strict_types=1);
4950

5051
use Ergebnis\Json\Pointer;
5152

52-
$referenceToken = Pointer\ReferenceToken::fromJsonString('foo~1bar');
53+
$referenceToken = Pointer\ReferenceToken::fromJsonString('foo~19000~😆');
5354

54-
$referenceToken->toJsonString(); // 'foo~1bar'
55-
$referenceToken->toString(); // 'foo/bar'
55+
$referenceToken->toJsonString(); // 'foo~19000~😆'
56+
$referenceToken->toString(); // 'foo/9000/😆'
57+
$referenceToken->toUriFragmentIdentifierString(); // 'foo~19000~1%F0%9F%98%86'
58+
```
59+
60+
You can create a `ReferenceToken` from a [URI fragmend identifier `string` value](https://datatracker.ietf.org/doc/html/rfc6901#section-6):
61+
62+
```php
63+
<?php
64+
65+
declare(strict_types=1);
66+
67+
use Ergebnis\Json\Pointer;
68+
69+
$referenceToken = Pointer\ReferenceToken::fromUriFragmentIdentifierString('foo~19000~1%F0%9F%98%86');
70+
71+
$referenceToken->toJsonString(); // 'foo~19000~😆'
72+
$referenceToken->toString(); // 'foo/9000/😆'
73+
$referenceToken->toUriFragmentIdentifierString(); // 'foo~19000~1%F0%9F%98%86'
5674
```
5775

5876
You can create a `ReferenceToken` from an `int` value:
@@ -66,8 +84,9 @@ use Ergebnis\Json\Pointer;
6684

6785
$referenceToken = Pointer\ReferenceToken::fromInt(9001);
6886

69-
$referenceToken->toJsonString(); // '9001'
70-
$referenceToken->toString(); // '9001'
87+
$referenceToken->toJsonString(); // '9001'
88+
$referenceToken->toString(); // '9001'
89+
$referenceToken->toUriFragmentIdentifierString(); // '9001'
7190
```
7291

7392
You can compare `ReferenceToken`s:
@@ -98,7 +117,8 @@ use Ergebnis\Json\Pointer;
98117

99118
$jsonPointer = Pointer\JsonPointer::document();
100119

101-
$jsonPointer->toJsonString(); // ''
120+
$jsonPointer->toJsonString(); // ''
121+
$jsonPointer->toUriFragmentIdentifierString(); // '#'
102122
```
103123

104124
You can create a `JsonPointer` from a [JSON `string` representation](https://datatracker.ietf.org/doc/html/rfc6901#section-5) value:
@@ -110,9 +130,26 @@ declare(strict_types=1);
110130

111131
use Ergebnis\Json\Pointer;
112132

113-
$jsonPointer = Pointer\JsonPointer::fromJsonString('/foo/bar');
133+
$jsonPointer = Pointer\JsonPointer::fromJsonString('/foo/bar/😆');
134+
135+
$jsonPointer->toJsonString(); // '/foo/bar/😆'
136+
$jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar/%F0%9F%98%86'
137+
```
138+
139+
You can create a `JsonPointer` from a [URI fragment identifier `string` representation](https://datatracker.ietf.org/doc/html/rfc6901#section-6) value:
140+
141+
```php
142+
<?php
143+
144+
declare(strict_types=1);
145+
146+
use Ergebnis\Json\Pointer;
147+
148+
$jsonPointer = Pointer\JsonPointer::fromJsonString('#/foo/bar/%F0%9F%98%86');
114149

115-
$jsonPointer->toJsonString(); // '/foo/bar'
150+
$jsonPointer->toJsonString(); // '/foo/bar/😆'
151+
$jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar/%F0%9F%98%86'
152+
$jsonPointer = Pointer\JsonPointer::fromUriFragmentIdentifierString('#foo/bar');
116153
```
117154

118155
You can create a `JsonPointer` from `ReferenceToken`s:
@@ -131,7 +168,8 @@ $referenceTokens = [
131168

132169
$jsonPointer = Pointer\JsonPointer::fromReferenceTokens(...$referenceTokens);
133170

134-
$jsonPointer->toJsonString(); // '/foo/bar'
171+
$jsonPointer->toJsonString(); // '/foo/bar'
172+
$jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar'
135173
```
136174
You can compare `JsonPointer`s:
137175

@@ -163,7 +201,8 @@ $referenceToken = Pointer\ReferenceToken::fromString('baz');
163201

164202
$newJsonPointer = $jsonPointer->append($referenceToken);
165203

166-
$newJsonPointer->toJsonString(); // '/foo/bar/baz'
204+
$newJsonPointer->toJsonString(); // '/foo/bar/baz'
205+
$newJsonPointer->toUriFragmentIdentifierString(); // '#foo/bar/baz'
167206
```
168207

169208
## Changelog

psalm-baseline.xml

+7
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@
55
<code>$referenceTokens</code>
66
</MixedPropertyTypeCoercion>
77
</file>
8+
<file src="test/Unit/ReferenceTokenTest.php">
9+
<TooFewArguments occurrences="3">
10+
<code>testFromJsonStringReturnsReferenceToken</code>
11+
<code>testFromStringReturnsReferenceToken</code>
12+
<code>testFromUriFragmentIdentifierStringReturnsReferenceToken</code>
13+
</TooFewArguments>
14+
</file>
815
</files>

src/Exception/InvalidJsonPointer.php

+8
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,12 @@ public static function fromJsonString(string $value): self
2222
$value,
2323
));
2424
}
25+
26+
public static function fromUriFragmentIdentifierString(string $value): self
27+
{
28+
return new self(\sprintf(
29+
'Value "%s" does not appear to be a valid URI fragment identifier representation of a JSON Pointer.',
30+
$value,
31+
));
32+
}
2533
}

src/JsonPointer.php

+38-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private function __construct(ReferenceToken ...$referenceTokens)
3838
*/
3939
public static function fromJsonString(string $value): self
4040
{
41-
if (1 !== \preg_match(Pattern::JSON_POINTER_JSON_STRING, $value)) {
41+
if (1 !== \preg_match(Pattern::JSON_STRING_JSON_POINTER, $value)) {
4242
throw Exception\InvalidJsonPointer::fromJsonString($value);
4343
}
4444

@@ -57,6 +57,29 @@ public static function fromReferenceTokens(ReferenceToken ...$referenceTokens):
5757
return new self(...$referenceTokens);
5858
}
5959

60+
/**
61+
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-3
62+
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-5
63+
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
64+
*
65+
* @throws Exception\InvalidJsonPointer
66+
*/
67+
public static function fromUriFragmentIdentifierString(string $value): self
68+
{
69+
if (1 !== \preg_match(Pattern::URI_FRAGMENT_IDENTIFIER_JSON_POINTER, $value)) {
70+
throw Exception\InvalidJsonPointer::fromJsonString($value);
71+
}
72+
73+
$uriFragmentIdentifierStringValues = \array_slice(
74+
\explode('/', $value),
75+
1,
76+
);
77+
78+
return new self(...\array_map(static function (string $uriFragmentIdentifierStringValue): ReferenceToken {
79+
return ReferenceToken::fromUriFragmentIdentifierString($uriFragmentIdentifierStringValue);
80+
}, $uriFragmentIdentifierStringValues));
81+
}
82+
6083
public static function document(): self
6184
{
6285
return new self();
@@ -85,6 +108,20 @@ public function toJsonString(): string
85108
);
86109
}
87110

111+
public function toUriFragmentIdentifierString(): string
112+
{
113+
if ([] === $this->referenceTokens) {
114+
return '#';
115+
}
116+
117+
return \sprintf(
118+
'#/%s',
119+
\implode('/', \array_map(static function (ReferenceToken $referenceToken): string {
120+
return $referenceToken->toUriFragmentIdentifierString();
121+
}, $this->referenceTokens)),
122+
);
123+
}
124+
88125
/**
89126
* @return array<int, ReferenceToken>
90127
*/

src/Pattern.php

+12-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,20 @@ final class Pattern
2121
/**
2222
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-3
2323
*/
24-
public const JSON_POINTER_JSON_STRING = '/^(?P<jsonPointer>(\/(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*))*)$/u';
24+
public const JSON_STRING_JSON_POINTER = '/^(?P<jsonStringJsonPointer>(\/(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*))*)$/u';
2525

2626
/**
2727
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-3
2828
*/
29-
public const REFERENCE_TOKEN = '/^(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*)$/u';
29+
public const JSON_STRING_REFERENCE_TOKEN = '/^(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*)$/u';
30+
public const URI_FRAGMENT_IDENTIFIER_JSON_POINTER = '/^(?P<uriFragmentIdentifierJsonPointer>#(\/(?P<referenceToken>((?P<pchar>((?P<unreserved>((?P<alpha>[a-zA-Z])|(?P<digit>\d)|-|\.|_|~))|(?P<pctEncoded>%(?P<hexDig>[0-9a-fA-F]){2})|(?P<subDelims>(!|\$|&|\'|\(|\)|\*|\+|,|;|=))|:|@))*)))*)$/u';
31+
32+
/**
33+
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-6
34+
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
35+
* @see https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
36+
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
37+
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
38+
*/
39+
public const URI_FRAGMENT_IDENTIFIER_REFERENCE_TOKEN = '/^(?P<referenceToken>((?P<pchar>((?P<unreserved>((?P<alpha>[a-zA-Z])|(?P<digit>\d)|-|\.|_|~))|(?P<pctEncoded>%(?P<hexDig>[0-9a-fA-F]){2})|(?P<subDelims>(!|\$|&|\'|\(|\)|\*|\+|,|;|=))|:|@))*))$/u';
3040
}

src/ReferenceToken.php

+38-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static function fromInt(int $value): self
4646
*/
4747
public static function fromJsonString(string $value): self
4848
{
49-
if (1 !== \preg_match(Pattern::REFERENCE_TOKEN, $value)) {
49+
if (1 !== \preg_match(Pattern::JSON_STRING_REFERENCE_TOKEN, $value)) {
5050
throw Exception\InvalidReferenceToken::fromJsonString($value);
5151
}
5252

@@ -68,6 +68,28 @@ public static function fromString(string $value): self
6868
return new self($value);
6969
}
7070

71+
/**
72+
* @throws Exception\InvalidReferenceToken
73+
*/
74+
public static function fromUriFragmentIdentifierString(string $value): self
75+
{
76+
if (1 !== \preg_match(Pattern::URI_FRAGMENT_IDENTIFIER_REFERENCE_TOKEN, $value)) {
77+
throw Exception\InvalidReferenceToken::fromJsonString($value);
78+
}
79+
80+
return new self(\str_replace(
81+
[
82+
'~1',
83+
'~0',
84+
],
85+
[
86+
'/',
87+
'~',
88+
],
89+
\rawurldecode($value),
90+
));
91+
}
92+
7193
public function toJsonString(): string
7294
{
7395
return \str_replace(
@@ -83,6 +105,21 @@ public function toJsonString(): string
83105
);
84106
}
85107

108+
public function toUriFragmentIdentifierString(): string
109+
{
110+
return \rawurlencode(\str_replace(
111+
[
112+
'~',
113+
'/',
114+
],
115+
[
116+
'~0',
117+
'~1',
118+
],
119+
$this->value,
120+
));
121+
}
122+
86123
public function toString(): string
87124
{
88125
return $this->value;

test/Unit/Exception/InvalidJsonPointerTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,18 @@ public function testFromJsonStringReturnsInvalidJsonPointerException(): void
3939

4040
self::assertSame($message, $exception->getMessage());
4141
}
42+
43+
public function testFromUriFragmentIdentifierStringReturnsInvalidJsonPointerException(): void
44+
{
45+
$value = self::faker()->sentence();
46+
47+
$exception = Exception\InvalidJsonPointer::fromUriFragmentIdentifierString($value);
48+
49+
$message = \sprintf(
50+
'Value "%s" does not appear to be a valid URI fragment identifier representation of a JSON Pointer.',
51+
$value,
52+
);
53+
54+
self::assertSame($message, $exception->getMessage());
55+
}
4256
}

0 commit comments

Comments
 (0)