Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: Add support for URI fragment identifiers #6

Merged
merged 1 commit into from
Jan 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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