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 bbd9be9
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 28 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
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ use Ergebnis\Json\Pointer;

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

$referenceToken->toJsonString(); // 'foo~1bar'
$referenceToken->toString(); // 'foo/bar'
$referenceToken->toJsonString(); // 'foo~1bar'
$referenceToken->toString(); // 'foo/bar'
$referenceToken->toUriFragmentIdentifierString(); // 'foo~1bar'
```

You can create a `ReferenceToken` from a [JSON `string` value](https://datatracker.ietf.org/doc/html/rfc6901#section-5):
Expand All @@ -51,8 +52,25 @@ use Ergebnis\Json\Pointer;

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

$referenceToken->toJsonString(); // 'foo~1bar'
$referenceToken->toString(); // 'foo/bar'
$referenceToken->toJsonString(); // 'foo~1bar'
$referenceToken->toString(); // 'foo/bar'
$referenceToken->toUriFragmentIdentifierString(); // 'foo~1bar'
```

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/bar');

$referenceToken->toJsonString(); // 'foo~1bar'
$referenceToken->toString(); // 'foo/bar'
$referenceToken->toUriFragmentIdentifierString(); // 'foo~1bar'
```

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 @@ -112,7 +131,23 @@ use Ergebnis\Json\Pointer;

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

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

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');

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

You can create a `JsonPointer` from `ReferenceToken`s:
Expand Down Expand Up @@ -163,7 +198,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
12 changes: 12 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@
<code>$referenceTokens</code>
</MixedPropertyTypeCoercion>
</file>
<file src="src/ReferenceToken.php">
<UnusedVariable occurrences="1">
<code>$fragment</code>
</UnusedVariable>
</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,
));
}
}
37 changes: 37 additions & 0 deletions src/JsonPointer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('/^#(\/(?P<referenceToken>((?P<unescaped>[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P<escaped>~[01]))*))*$/u', $value, $matches)) {
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
2 changes: 1 addition & 1 deletion src/Pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ final class Pattern
/**
* @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';
}
53 changes: 52 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,42 @@ public static function fromString(string $value): self
return new self($value);
}

/**
* @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
*
* @throws Exception\InvalidReferenceToken
*/
public static function fromUriFragmentIdentifierString(string $value): self
{
// fragment = *( pchar / "/" / "?" )
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
// pct-encoded = "%" HEXDIG HEXDIG
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="

$fragment = '(?P<pchar>(unreserved|pct-encoded|sub-delims|:|@)|/|\?)';

if (1 !== \preg_match('/^(?P<fragment>()*)$/u', $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 +119,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 bbd9be9

Please sign in to comment.