Skip to content

Commit

Permalink
Enhancement: Add support for URI fragment identifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
localheinz committed Jan 29, 2022
1 parent d545054 commit 7caebfa
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 39 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ For a full diff see [`1.0.0...2.0.0`][1.0.0...2.0.0].

## Added

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

## Changed
Expand Down Expand Up @@ -45,6 +46,7 @@ For a full diff see [`a5ba52c...1.0.0`][a5ba52c...1.0.0].
[#2]: https://github.com/ergebnis/json-pointer/pull/2
[#4]: https://github.com/ergebnis/json-pointer/pull/4
[#5]: https://github.com/ergebnis/json-pointer/pull/5
[#6]: https://github.com/ergebnis/json-pointer/pull/6
[#9]: https://github.com/ergebnis/json-pointer/pull/9

[@localheinz]: https://github.com/localheinz
65 changes: 52 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ declare(strict_types=1);

use Ergebnis\Json\Pointer;

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

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

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

use Ergebnis\Json\Pointer;

$referenceToken = Pointer\ReferenceToken::fromJsonString('foo~1bar');
$referenceToken = Pointer\ReferenceToken::fromJsonString('foo~19000~😆');

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

You can create a `ReferenceToken` from a [URI fragmend identifier `string` value](https://datatracker.ietf.org/doc/html/rfc6901#section-6):

```php
<?php

declare(strict_types=1);

use Ergebnis\Json\Pointer;

$referenceToken = Pointer\ReferenceToken::fromUriFragmentIdentifierString('foo~19000~1%F0%9F%98%86');

$referenceToken->toJsonString(); // 'foo~19000~😆'
$referenceToken->toString(); // 'foo/9000/😆'
$referenceToken->toUriFragmentIdentifierString(); // 'foo~19000~1%F0%9F%98%86'
```

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

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

$referenceToken->toJsonString(); // '9001'
$referenceToken->toString(); // '9001'
$referenceToken->toJsonString(); // '9001'
$referenceToken->toString(); // '9001'
$referenceToken->toUriFragmentIdentifierString(); // '9001'
```

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

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

$jsonPointer->toJsonString(); // ''
$jsonPointer->toJsonString(); // ''
$jsonPointer->toUriFragmentIdentifierString(); // '#'
```

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

use Ergebnis\Json\Pointer;

$jsonPointer = Pointer\JsonPointer::fromJsonString('/foo/bar');
$jsonPointer = Pointer\JsonPointer::fromJsonString('/foo/bar/😆');

$jsonPointer->toJsonString(); // '/foo/bar/😆'
$jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar/%F0%9F%98%86'
```

You can create a `JsonPointer` from a [URI fragment identifier `string` representation](https://datatracker.ietf.org/doc/html/rfc6901#section-6) value:

```php
<?php

declare(strict_types=1);

use Ergebnis\Json\Pointer;

$jsonPointer = Pointer\JsonPointer::fromJsonString('#/foo/bar/%F0%9F%98%86');

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

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

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

$jsonPointer->toJsonString(); // '/foo/bar'
$jsonPointer->toJsonString(); // '/foo/bar'
$jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar'
```
You can compare `JsonPointer`s:

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

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

$newJsonPointer->toJsonString(); // '/foo/bar/baz'
$newJsonPointer->toJsonString(); // '/foo/bar/baz'
$newJsonPointer->toUriFragmentIdentifierString(); // '#foo/bar/baz'
```

## Changelog
Expand Down
7 changes: 7 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@
<code>$referenceTokens</code>
</MixedPropertyTypeCoercion>
</file>
<file src="test/Unit/ReferenceTokenTest.php">
<TooFewArguments occurrences="3">
<code>testFromJsonStringReturnsReferenceToken</code>
<code>testFromStringReturnsReferenceToken</code>
<code>testFromUriFragmentIdentifierStringReturnsReferenceToken</code>
</TooFewArguments>
</file>
</files>
8 changes: 8 additions & 0 deletions src/Exception/InvalidJsonPointer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ public static function fromJsonString(string $value): self
$value,
));
}

public static function fromUriFragmentIdentifierString(string $value): self
{
return new self(\sprintf(
'Value "%s" does not appear to be a valid URI fragment identifier representation of a JSON Pointer.',
$value,
));
}
}
39 changes: 38 additions & 1 deletion src/JsonPointer.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private function __construct(ReferenceToken ...$referenceTokens)
*/
public static function fromJsonString(string $value): self
{
if (1 !== \preg_match(Pattern::JSON_POINTER_JSON_STRING, $value)) {
if (1 !== \preg_match(Pattern::JSON_STRING_JSON_POINTER, $value)) {
throw Exception\InvalidJsonPointer::fromJsonString($value);
}

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

/**
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-3
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-5
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
*
* @throws Exception\InvalidJsonPointer
*/
public static function fromUriFragmentIdentifierString(string $value): self
{
if (1 !== \preg_match(Pattern::URI_FRAGMENT_IDENTIFIER_JSON_POINTER, $value)) {
throw Exception\InvalidJsonPointer::fromJsonString($value);
}

$uriFragmentIdentifierStringValues = \array_slice(
\explode('/', $value),
1,
);

return new self(...\array_map(static function (string $uriFragmentIdentifierStringValue): ReferenceToken {
return ReferenceToken::fromUriFragmentIdentifierString($uriFragmentIdentifierStringValue);
}, $uriFragmentIdentifierStringValues));
}

public static function document(): self
{
return new self();
Expand Down Expand Up @@ -85,6 +108,20 @@ public function toJsonString(): string
);
}

public function toUriFragmentIdentifierString(): string
{
if ([] === $this->referenceTokens) {
return '#';
}

return \sprintf(
'#/%s',
\implode('/', \array_map(static function (ReferenceToken $referenceToken): string {
return $referenceToken->toUriFragmentIdentifierString();
}, $this->referenceTokens)),
);
}

/**
* @return array<int, ReferenceToken>
*/
Expand Down
14 changes: 12 additions & 2 deletions src/Pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,20 @@ final class Pattern
/**
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-3
*/
public const JSON_POINTER_JSON_STRING = '/^(?P<jsonPointer>(\/(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*))*)$/u';
public const JSON_STRING_JSON_POINTER = '/^(?P<jsonStringJsonPointer>(\/(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*))*)$/u';

/**
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-3
*/
public const REFERENCE_TOKEN = '/^(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*)$/u';
public const JSON_STRING_REFERENCE_TOKEN = '/^(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*)$/u';
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';

/**
* @see https://datatracker.ietf.org/doc/html/rfc6901#section-6
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
* @see https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
*/
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';
}
39 changes: 38 additions & 1 deletion src/ReferenceToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static function fromInt(int $value): self
*/
public static function fromJsonString(string $value): self
{
if (1 !== \preg_match(Pattern::REFERENCE_TOKEN, $value)) {
if (1 !== \preg_match(Pattern::JSON_STRING_REFERENCE_TOKEN, $value)) {
throw Exception\InvalidReferenceToken::fromJsonString($value);
}

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

/**
* @throws Exception\InvalidReferenceToken
*/
public static function fromUriFragmentIdentifierString(string $value): self
{
if (1 !== \preg_match(Pattern::URI_FRAGMENT_IDENTIFIER_REFERENCE_TOKEN, $value)) {
throw Exception\InvalidReferenceToken::fromJsonString($value);
}

return new self(\str_replace(
[
'~1',
'~0',
],
[
'/',
'~',
],
\rawurldecode($value),
));
}

public function toJsonString(): string
{
return \str_replace(
Expand All @@ -83,6 +105,21 @@ public function toJsonString(): string
);
}

public function toUriFragmentIdentifierString(): string
{
return \rawurlencode(\str_replace(
[
'~',
'/',
],
[
'~0',
'~1',
],
$this->value,
));
}

public function toString(): string
{
return $this->value;
Expand Down
14 changes: 14 additions & 0 deletions test/Unit/Exception/InvalidJsonPointerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,18 @@ public function testFromJsonStringReturnsInvalidJsonPointerException(): void

self::assertSame($message, $exception->getMessage());
}

public function testFromUriFragmentIdentifierStringReturnsInvalidJsonPointerException(): void
{
$value = self::faker()->sentence();

$exception = Exception\InvalidJsonPointer::fromUriFragmentIdentifierString($value);

$message = \sprintf(
'Value "%s" does not appear to be a valid URI fragment identifier representation of a JSON Pointer.',
$value,
);

self::assertSame($message, $exception->getMessage());
}
}
Loading

0 comments on commit 7caebfa

Please sign in to comment.