From bbd9be93cb0a256ff89bb2978fb9cff7a7008083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Sat, 29 Jan 2022 18:13:02 +0100 Subject: [PATCH] Enhancement: Add support for URI fragment identifiers --- CHANGELOG.md | 2 + README.md | 52 +++++++++-- psalm-baseline.xml | 12 +++ src/Exception/InvalidJsonPointer.php | 8 ++ src/JsonPointer.php | 37 ++++++++ src/Pattern.php | 2 +- src/ReferenceToken.php | 53 ++++++++++- .../Unit/Exception/InvalidJsonPointerTest.php | 14 +++ test/Unit/JsonPointerTest.php | 91 +++++++++++++++++-- test/Unit/PatternTest.php | 10 +- test/Unit/ReferenceTokenTest.php | 88 ++++++++++++++++-- 11 files changed, 341 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 669b2172..d3d4226a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index f312bb29..6167598b 100644 --- a/README.md +++ b/README.md @@ -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): @@ -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 +toJsonString(); // 'foo~1bar' +$referenceToken->toString(); // 'foo/bar' +$referenceToken->toUriFragmentIdentifierString(); // 'foo~1bar' ``` You can create a `ReferenceToken` from an `int` value: @@ -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: @@ -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 +toJsonString(); // '/foo/bar' +$jsonPointer->toUriFragmentIdentifierString(); // '#foo/bar' ``` You can create a `JsonPointer` from `ReferenceToken`s: @@ -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 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7075e2a3..eb97be82 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -5,4 +5,16 @@ $referenceTokens + + + $fragment + + + + + testFromJsonStringReturnsReferenceToken + testFromStringReturnsReferenceToken + testFromUriFragmentIdentifierStringReturnsReferenceToken + + diff --git a/src/Exception/InvalidJsonPointer.php b/src/Exception/InvalidJsonPointer.php index 677bb0af..74c08e47 100644 --- a/src/Exception/InvalidJsonPointer.php +++ b/src/Exception/InvalidJsonPointer.php @@ -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, + )); + } } diff --git a/src/JsonPointer.php b/src/JsonPointer.php index 5f06ea5a..9f364383 100644 --- a/src/JsonPointer.php +++ b/src/JsonPointer.php @@ -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((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[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(); @@ -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 */ diff --git a/src/Pattern.php b/src/Pattern.php index 3ebd79ce..8957e777 100644 --- a/src/Pattern.php +++ b/src/Pattern.php @@ -26,5 +26,5 @@ final class Pattern /** * @see https://datatracker.ietf.org/doc/html/rfc6901#section-3 */ - public const REFERENCE_TOKEN = '/^(?P((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[01]))*)$/u'; + public const JSON_STRING_REFERENCE_TOKEN = '/^(?P((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[01]))*)$/u'; } diff --git a/src/ReferenceToken.php b/src/ReferenceToken.php index 199de6bf..cef4b5d0 100644 --- a/src/ReferenceToken.php +++ b/src/ReferenceToken.php @@ -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); } @@ -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(unreserved|pct-encoded|sub-delims|:|@)|/|\?)'; + + if (1 !== \preg_match('/^(?P()*)$/u', $value)) { + throw Exception\InvalidReferenceToken::fromJsonString($value); + } + + return new self(\str_replace( + [ + '~1', + '~0', + ], + [ + '/', + '~', + ], + \rawurldecode($value), + )); + } + public function toJsonString(): string { return \str_replace( @@ -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; diff --git a/test/Unit/Exception/InvalidJsonPointerTest.php b/test/Unit/Exception/InvalidJsonPointerTest.php index 18d90ed6..3758db29 100644 --- a/test/Unit/Exception/InvalidJsonPointerTest.php +++ b/test/Unit/Exception/InvalidJsonPointerTest.php @@ -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()); + } } diff --git a/test/Unit/JsonPointerTest.php b/test/Unit/JsonPointerTest.php index dcc15143..98ed3063 100644 --- a/test/Unit/JsonPointerTest.php +++ b/test/Unit/JsonPointerTest.php @@ -77,133 +77,170 @@ public function provideInvalidJsonStringValue(): \Generator } /** - * @dataProvider provideJsonStringValueAndReferenceTokens + * @dataProvider provideJsonStringValueUriFragmentIdentifierStringValueAndReferenceTokens * * @param array $referenceTokens */ public function testFromJsonStringReturnsJsonPointer( string $jsonStringValue, + string $uriFragmentIdentifierStringValue, array $referenceTokens ): void { $jsonPointer = JsonPointer::fromJsonString($jsonStringValue); self::assertSame($jsonStringValue, $jsonPointer->toJsonString()); + self::assertSame($uriFragmentIdentifierStringValue, $jsonPointer->toUriFragmentIdentifierString()); self::assertEquals($referenceTokens, $jsonPointer->toReferenceTokens()); } /** - * @dataProvider provideJsonStringValueAndReferenceTokens + * @dataProvider provideJsonStringValueUriFragmentIdentifierStringValueAndReferenceTokens + * + * @param array $referenceTokens + */ + public function testFromUriFragmentIdentifierStringReturnsJsonPointer( + string $jsonStringValue, + string $uriFragmentIdentifierStringValue, + array $referenceTokens + ): void { + $jsonPointer = JsonPointer::fromUriFragmentIdentifierString($uriFragmentIdentifierStringValue); + + self::assertSame($jsonStringValue, $jsonPointer->toJsonString()); + self::assertSame($uriFragmentIdentifierStringValue, $jsonPointer->toUriFragmentIdentifierString()); + self::assertEquals($referenceTokens, $jsonPointer->toReferenceTokens()); + } + + /** + * @dataProvider provideJsonStringValueUriFragmentIdentifierStringValueAndReferenceTokens * * @param array $referenceTokens */ public function testFromReferenceTokensReturnsJsonPointer( string $jsonStringValue, + string $uriFragmentIdentifierStringValue, array $referenceTokens ): void { $jsonPointer = JsonPointer::fromReferenceTokens(...$referenceTokens); self::assertSame($jsonStringValue, $jsonPointer->toJsonString()); + self::assertSame($uriFragmentIdentifierStringValue, $jsonPointer->toUriFragmentIdentifierString()); self::assertEquals($referenceTokens, $jsonPointer->toReferenceTokens()); } /** * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 * - * @return \Generator}> + * @return \Generator}> */ - public function provideJsonStringValueAndReferenceTokens(): \Generator + public function provideJsonStringValueUriFragmentIdentifierStringValueAndReferenceTokens(): \Generator { $values = [ 'array-index-0' => [ '/0', + '#/0', [ ReferenceToken::fromInt(0), ], ], 'array-index-1' => [ '/1', + '#/1', [ ReferenceToken::fromInt(1), ], ], 'array-index-9000' => [ '/9000', + '#/9000', [ ReferenceToken::fromInt(9000), ], ], 'document' => [ '', + '#', [], ], 'document-root' => [ '/', + '#/', [ ReferenceToken::fromString(''), ], ], 'property-caret' => [ '/^', + '#/%5E', [ ReferenceToken::fromString('^'), ], ], 'property-percent' => [ '/%', + '#/%25', [ ReferenceToken::fromString('%'), ], ], 'property-pipe' => [ '/|', + '#/%7C', [ ReferenceToken::fromString('|'), ], ], 'property-quote-double' => [ '/"', + '#/%22', [ ReferenceToken::fromString('"'), ], ], 'property-quote-single' => [ "/'", + '#/%27', [ ReferenceToken::fromString("'"), ], ], 'property-slash-backward' => [ '/\\', + '#/%5C', [ ReferenceToken::fromString('\\'), ], ], 'property-slash-forward-escaped' => [ '/~1', + '#/~1', [ ReferenceToken::fromString('/'), ], ], 'property-space' => [ '/ ', + '#/%20', [ ReferenceToken::fromString(' '), ], ], 'property-tilde-escaped' => [ '/~0', + '#/~0', [ ReferenceToken::fromString('~'), ], ], 'property-unicode-character' => [ '/😆', + '#/%F0%9F%98%86', [ ReferenceToken::fromString('😆'), ], ], 'property-with-array-index' => [ '/foo/0', + '#/foo/0', [ ReferenceToken::fromString('foo'), ReferenceToken::fromInt(0), @@ -211,60 +248,70 @@ public function provideJsonStringValueAndReferenceTokens(): \Generator ], 'property-with-caret' => [ '/foo^bar', + '#/foo%5Ebar', [ ReferenceToken::fromString('foo^bar'), ], ], 'property-with-percent' => [ '/foo%bar', + '#/foo%25bar', [ ReferenceToken::fromString('foo%bar'), ], ], 'property-with-pipe' => [ '/foo|bar', + '#/foo%7Cbar', [ ReferenceToken::fromString('foo|bar'), ], ], 'property-with-quote-double' => [ '/foo"bar', + '#/foo%22bar', [ ReferenceToken::fromString('foo"bar'), ], ], 'property-with-quote-single' => [ "/foo'bar", + '#/foo%27bar', [ ReferenceToken::fromString("foo'bar"), ], ], 'property-with-slash-backward' => [ '/foo\\bar', + '#/foo%5Cbar', [ ReferenceToken::fromString('foo\\bar'), ], ], 'property-with-slash-forward-escaped' => [ '/foo~1bar', + '#/foo~1bar', [ ReferenceToken::fromString('foo/bar'), ], ], 'property-with-tilde-escaped' => [ '/foo~0bar', + '#/foo~0bar', [ ReferenceToken::fromString('foo~bar'), ], ], 'property-with-unicode-character' => [ '/foo😆bar', + '#/foo%F0%9F%98%86bar', [ ReferenceToken::fromString('foo😆bar'), ], ], 'property-with-word' => [ '/foo/bar', + '#/foo/bar', [ ReferenceToken::fromString('foo'), ReferenceToken::fromString('bar'), @@ -272,20 +319,52 @@ public function provideJsonStringValueAndReferenceTokens(): \Generator ], 'property-word' => [ '/foo', + '#/foo', [ ReferenceToken::fromString('foo'), ], ], ]; - foreach ($values as $key => [$value, $referenceTokens]) { + foreach ($values as $key => [$jsonStringValue, $uriFragmentIdentifierStringValue,$referenceTokens]) { yield $key => [ - $value, + $jsonStringValue, + $uriFragmentIdentifierStringValue, $referenceTokens, ]; } } + /** + * @dataProvider provideInvalidUriFragmentIdentifierStringValue + */ + public function testFromUriFragmentIdentifierStringRejectsInvalidValue(string $value): void + { + $this->expectException(Exception\InvalidJsonPointer::class); + + JsonPointer::fromUriFragmentIdentifierString($value); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 + * + * @return \Generator + */ + public function provideInvalidUriFragmentIdentifierStringValue(): \Generator + { + $values = [ + 'does-not-start-with-hash' => 'foo', + 'property-with-unescaped-tilde' => '#foo~bar', + 'property-with-unescaped-tildes' => '#foo~~bar', + ]; + + foreach ($values as $key => $value) { + yield $key => [ + $value, + ]; + } + } + public function testDocumentReturnsJsonPointer(): void { $jsonPointer = JsonPointer::document(); diff --git a/test/Unit/PatternTest.php b/test/Unit/PatternTest.php index 2c08462b..12b3c953 100644 --- a/test/Unit/PatternTest.php +++ b/test/Unit/PatternTest.php @@ -25,26 +25,26 @@ final class PatternTest extends Framework\TestCase { public function testJsonPointerJsonStringEqualsPattern(): void { - $referenceToken = self::referenceToken(); + $referenceToken = self::jsonStringReferenceToken(); $expected = "/^(?P(\\/{$referenceToken})*)$/u"; self::assertSame($expected, Pattern::JSON_POINTER_JSON_STRING); } - public function testReferenceTokenEqualsPattern(): void + public function testJsonStringReferenceTokenEqualsPattern(): void { - $referenceToken = self::referenceToken(); + $referenceToken = self::jsonStringReferenceToken(); $expected = "/^{$referenceToken}$/u"; - self::assertSame($expected, Pattern::REFERENCE_TOKEN); + self::assertSame($expected, Pattern::JSON_STRING_REFERENCE_TOKEN); } /** * @see https://datatracker.ietf.org/doc/html/rfc6901#section-3 */ - private static function referenceToken(): string + private static function jsonStringReferenceToken(): string { $unescaped = '(?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])'; $escaped = '(?P~[01])'; diff --git a/test/Unit/ReferenceTokenTest.php b/test/Unit/ReferenceTokenTest.php index 23cd2d88..5c2717e4 100644 --- a/test/Unit/ReferenceTokenTest.php +++ b/test/Unit/ReferenceTokenTest.php @@ -75,141 +75,215 @@ public function provideInvalidJsonStringValue(): \Generator } /** - * @dataProvider provideValueAndJsonStringValue + * @dataProvider provideInvalidUriFragmentIdentifierStringValue + */ + public function testFromUriFragmentIdentifierStringRejectsInvalidValue(string $value): void + { + $this->expectException(Exception\InvalidReferenceToken::class); + + ReferenceToken::fromUriFragmentIdentifierString($value); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-6 + * + * @return \Generator + */ + public function provideInvalidUriFragmentIdentifierStringValue(): \Generator + { + $values = [ + 'property-with-unescaped-forward-slash' => 'foo/bar', + 'property-with-unescaped-tilde' => 'foo~bar', + ]; + + foreach ($values as $key => $value) { + yield $key => [ + $value, + ]; + } + } + + /** + * @dataProvider provideValueJsonStringValueAndUriFragmentIdentifierStringValue */ public function testFromJsonStringReturnsReferenceToken( string $value, - string $jsonStringValue + string $jsonStringValue, + string $uriFragmentIdentifierStringValue ): void { $referenceToken = ReferenceToken::fromJsonString($jsonStringValue); self::assertSame($jsonStringValue, $referenceToken->toJsonString()); self::assertSame($value, $referenceToken->toString()); + self::assertSame($uriFragmentIdentifierStringValue, $referenceToken->toUriFragmentIdentifierString()); } /** - * @dataProvider provideValueAndJsonStringValue + * @dataProvider provideValueJsonStringValueAndUriFragmentIdentifierStringValue */ public function testFromStringReturnsReferenceToken( string $value, - string $jsonStringValue + string $jsonStringValue, + string $uriFragmentIdentifierStringValue ): void { $referenceToken = ReferenceToken::fromString($value); self::assertSame($jsonStringValue, $referenceToken->toJsonString()); self::assertSame($value, $referenceToken->toString()); + self::assertSame($uriFragmentIdentifierStringValue, $referenceToken->toUriFragmentIdentifierString()); + } + + /** + * @dataProvider provideValueJsonStringValueAndUriFragmentIdentifierStringValue + */ + public function testFromUriFragmentIdentifierStringReturnsReferenceToken( + string $value, + string $jsonStringValue, + string $uriFragmentIdentifierStringValue + ): void { + $referenceToken = ReferenceToken::fromUriFragmentIdentifierString($uriFragmentIdentifierStringValue); + + self::assertSame($jsonStringValue, $referenceToken->toJsonString()); + self::assertSame($value, $referenceToken->toString()); + self::assertSame($uriFragmentIdentifierStringValue, $referenceToken->toUriFragmentIdentifierString()); } /** * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 + * @see https://datatracker.ietf.org/doc/html/rfc6901#section-6 * - * @return \Generator + * @return \Generator */ - public function provideValueAndJsonStringValue(): \Generator + public function provideValueJsonStringValueAndUriFragmentIdentifierStringValue(): \Generator { $values = [ 'array-index-0' => [ '0', '0', + '0', ], 'array-index-1' => [ '1', '1', + '1', ], 'array-index-9000' => [ '9000', '9000', + '9000', ], 'caret' => [ '^', '^', + '%5E', ], 'percent' => [ '%', '%', + '%25', ], 'pipe' => [ '|', '|', + '%7C', ], 'quote-double' => [ '"', '"', + '%22', ], 'quote-single' => [ "'", "'", + '%27', ], 'slash-backward' => [ '\\', '\\', + '%5C', ], 'slash-forward' => [ '/', '~1', + '~1', ], 'space' => [ ' ', ' ', + '%20', ], 'word' => [ 'foo', 'foo', + 'foo', ], 'tilde' => [ '~', '~0', + '~0', ], 'unicode-character' => [ '😆', '😆', + '%F0%9F%98%86', ], 'with-caret' => [ 'foo^bar', 'foo^bar', + 'foo%5Ebar', ], 'with-percent' => [ 'foo%bar', 'foo%bar', + 'foo%25bar', ], 'with-pipe' => [ 'foo|bar', 'foo|bar', + 'foo%7Cbar', ], 'with-quote-double' => [ 'foo"bar', 'foo"bar', + 'foo%22bar', ], 'with-quote-single' => [ "foo'bar", "foo'bar", + 'foo%27bar', ], 'with-slash-backward' => [ 'foo\\bar', 'foo\\bar', + 'foo%5Cbar', ], 'with-slash-forward' => [ 'foo/bar', 'foo~1bar', + 'foo~1bar', ], 'with-space' => [ 'foo bar', 'foo bar', + 'foo%20bar', ], 'with-tilde' => [ 'foo~bar', 'foo~0bar', + 'foo~0bar', ], 'with-unicode-character' => [ 'foo😆bar', 'foo😆bar', + 'foo%F0%9F%98%86bar', ], ]; - foreach ($values as $key => [$value, $jsonStringValue]) { + foreach ($values as $key => [$value, $jsonStringValue, $uriFragmentIdentifierStringValue]) { yield $key => [ $value, $jsonStringValue, + $uriFragmentIdentifierStringValue, ]; } }