├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Exception ├── Exception.php ├── InvalidJsonPointer.php └── InvalidReferenceToken.php ├── JsonPointer.php ├── Pattern.php ├── ReferenceToken.php └── Specification.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | For a full diff see [`3.6.0...main`][3.6.0...main]. 10 | 11 | ## [`3.6.0`][3.6.0] 12 | 13 | For a full diff see [`3.5.0...3.6.0`][3.5.0...3.6.0]. 14 | 15 | ### Added 16 | 17 | - Added support for PHP 8.4 ([#428]), by [@localheinz] 18 | 19 | ## [`3.5.0`][3.5.0] 20 | 21 | For a full diff see [`3.4.0...3.5.0`][3.4.0...3.5.0]. 22 | 23 | ### Changed 24 | 25 | - Allowed installation on PHP 8.4 ([#419]), by [@localheinz] 26 | 27 | ## [`3.4.0`][3.4.0] 28 | 29 | For a full diff see [`3.3.0...3.4.0`][3.3.0...3.4.0]. 30 | 31 | ### Changed 32 | 33 | - Added support for PHP 8.0 ([#339]), by [@localheinz] 34 | - Added support for PHP 7.4 ([#340]), by [@localheinz] 35 | 36 | ## [`3.3.0`][3.3.0] 37 | 38 | For a full diff see [`3.2.0...3.3.0`][3.2.0...3.3.0]. 39 | 40 | ### Changed 41 | 42 | - Dropped support for PHP 8.0 ([#209]), by [@localheinz] 43 | - Added support for PHP 8.3 ([#271]), by [@localheinz] 44 | 45 | ## [`3.2.0`][3.2.0] 46 | 47 | For a full diff see [`3.1.0...3.2.0`][3.1.0...3.2.0]. 48 | 49 | ### Added 50 | 51 | - Added `Specification::not()` ([#123]), by [@localheinz] 52 | 53 | ### Changed 54 | 55 | - Dropped support for PHP 7.4 ([#119]), by [@localheinz] 56 | 57 | ## [`3.1.0`][3.1.0] 58 | 59 | For a full diff see [`3.0.0...3.1.0`][3.0.0...3.1.0]. 60 | 61 | ### Added 62 | 63 | - Added `Specification::closure()` ([#56]), by [@localheinz] 64 | - Added `Specification::never()` ([#57]), by [@localheinz] 65 | - Added `Specification::always()` ([#58]), by [@localheinz] 66 | 67 | ## [`3.0.0`][3.0.0] 68 | 69 | For a full diff see [`2.1.0...3.0.0`][2.1.0...3.0.0]. 70 | 71 | ### Added 72 | 73 | - Added `Specification` ([#50]), by [@localheinz] 74 | - Added `Specification::anyOf()` ([#53]), by [@localheinz] 75 | 76 | ### Removed 77 | 78 | - Removed `JsonPointers` ([#48]), by [@localheinz] 79 | 80 | ## [`2.1.0`][2.1.0] 81 | 82 | For a full diff see [`2.0.0...2.1.0`][2.0.0...2.1.0]. 83 | 84 | ### Added 85 | 86 | - Added `JsonPointers` as a value object ([#17]), by [@localheinz] 87 | 88 | ## [`2.0.0`][2.0.0] 89 | 90 | For a full diff see [`1.0.0...2.0.0`][1.0.0...2.0.0]. 91 | 92 | ### Added 93 | 94 | - Added named constructors `JsonPointer::fromUriFragmentIdentifierString()` and `ReferenceToken::fromUriFragmentIdentifierString()` to allow creation from URI fragment identifier representations ([#6]), by [@localheinz] 95 | - Added named constructor `JsonPointer::fromReferenceTokens()` to allow creation of `JsonPointer` from `ReferenceToken`s ([#9]), by [@localheinz] 96 | 97 | ### Changed 98 | 99 | - Renamed named constructors and accessors of `Exception\InvalidJsonPointer`, `JsonPointer`, and `ReferenceToken` ([#4]) and ([#5]), by [@localheinz] 100 | 101 | - `Exception\InvalidJsonPointer::fromString()` to `Exception\InvalidJsonPointer::fromJsonString()` 102 | - `JsonPointer::fromString()` to `JsonPointer::fromJsonString()` 103 | - `JsonPointer::toString()` to `JsonPointer::toJsonString()` 104 | - `ReferenceToken::fromEscapedString()` to `ReferenceToken::fromJsonString()` 105 | - `ReferenceToken::fromUnescapedString()` to `ReferenceToken::fromString()` 106 | - `ReferenceToken::toEscapedString()` to `ReferenceToken::toJsonString()` 107 | - `ReferenceToken::toUnescapedString()` to `ReferenceToken::toString()` 108 | 109 | ## [`1.0.0`][1.0.0] 110 | 111 | For a full diff see [`a5ba52c...1.0.0`][a5ba52c...1.0.0]. 112 | 113 | ### Added 114 | 115 | - Added `ReferenceToken` as a value object ([#1]), by [@localheinz] 116 | - Added `JsonPointer` as a value object ([#2]), by [@localheinz] 117 | 118 | [1.0.0]: https://github.com/ergebnis/json-pointer/releases/tag/1.0.0 119 | [2.0.0]: https://github.com/ergebnis/json-pointer/releases/tag/2.0.0 120 | [2.1.0]: https://github.com/ergebnis/json-pointer/releases/tag/2.1.0 121 | [3.0.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.0.0 122 | [3.1.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.1.0 123 | [3.2.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.2.0 124 | [3.3.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.3.0 125 | [3.4.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.4.0 126 | [3.5.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.5.0 127 | [3.6.0]: https://github.com/ergebnis/json-pointer/releases/tag/3.6.0 128 | 129 | [a5ba52c...1.0.0]: https://github.com/ergebnis/json-pointer/compare/a5ba52c...1.0.0 130 | [1.0.0...main]: https://github.com/ergebnis/json-pointer/compare/1.0.0...main 131 | [2.0.0...2.1.0]: https://github.com/ergebnis/json-pointer/compare/2.0.0...2.1.0 132 | [2.1.0...3.0.0]: https://github.com/ergebnis/json-pointer/compare/2.1.0...3.0.0 133 | [3.0.0...3.1.0]: https://github.com/ergebnis/json-pointer/compare/3.0.0...3.1.0 134 | [3.1.0...3.2.0]: https://github.com/ergebnis/json-pointer/compare/3.1.0...3.2.0 135 | [3.2.0...3.3.0]: https://github.com/ergebnis/json-pointer/compare/3.2.0...3.3.0 136 | [3.3.0...3.4.0]: https://github.com/ergebnis/json-pointer/compare/3.3.0...3.4.0 137 | [3.4.0...3.5.0]: https://github.com/ergebnis/json-pointer/compare/3.4.0...3.5.0 138 | [3.5.0...3.6.0]: https://github.com/ergebnis/json-pointer/compare/3.5.0...3.6.0 139 | [3.6.0...main]: https://github.com/ergebnis/json-pointer/compare/3.6.0...main 140 | 141 | [#1]: https://github.com/ergebnis/json-pointer/pull/1 142 | [#2]: https://github.com/ergebnis/json-pointer/pull/2 143 | [#4]: https://github.com/ergebnis/json-pointer/pull/4 144 | [#5]: https://github.com/ergebnis/json-pointer/pull/5 145 | [#6]: https://github.com/ergebnis/json-pointer/pull/6 146 | [#9]: https://github.com/ergebnis/json-pointer/pull/9 147 | [#17]: https://github.com/ergebnis/json-pointer/pull/17 148 | [#48]: https://github.com/ergebnis/json-pointer/pull/48 149 | [#53]: https://github.com/ergebnis/json-pointer/pull/53 150 | [#56]: https://github.com/ergebnis/json-pointer/pull/56 151 | [#57]: https://github.com/ergebnis/json-pointer/pull/57 152 | [#58]: https://github.com/ergebnis/json-pointer/pull/58 153 | [#119]: https://github.com/ergebnis/json-pointer/pull/119 154 | [#123]: https://github.com/ergebnis/json-pointer/pull/123 155 | [#209]: https://github.com/ergebnis/json-pointer/pull/209 156 | [#271]: https://github.com/ergebnis/json-pointer/pull/271 157 | [#339]: https://github.com/ergebnis/json-pointer/pull/339 158 | [#340]: https://github.com/ergebnis/json-pointer/pull/340 159 | [#419]: https://github.com/ergebnis/json-pointer/pull/419 160 | [#428]: https://github.com/ergebnis/json-pointer/pull/428 161 | 162 | [@localheinz]: https://github.com/localheinz 163 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2022-2025 Andreas Möller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the _Software_), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED **AS IS**, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-pointer 2 | 3 | [![Integrate](https://github.com/ergebnis/json-pointer/workflows/Integrate/badge.svg)](https://github.com/ergebnis/json-pointer/actions) 4 | [![Merge](https://github.com/ergebnis/json-pointer/workflows/Merge/badge.svg)](https://github.com/ergebnis/json-pointer/actions) 5 | [![Release](https://github.com/ergebnis/json-pointer/workflows/Release/badge.svg)](https://github.com/ergebnis/json-pointer/actions) 6 | [![Renew](https://github.com/ergebnis/json-pointer/workflows/Renew/badge.svg)](https://github.com/ergebnis/json-pointer/actions) 7 | 8 | [![Code Coverage](https://codecov.io/gh/ergebnis/json-pointer/branch/main/graph/badge.svg)](https://codecov.io/gh/ergebnis/json-pointer) 9 | 10 | [![Latest Stable Version](https://poser.pugx.org/ergebnis/json-pointer/v/stable)](https://packagist.org/packages/ergebnis/json-pointer) 11 | [![Total Downloads](https://poser.pugx.org/ergebnis/json-pointer/downloads)](https://packagist.org/packages/ergebnis/json-pointer) 12 | [![Monthly Downloads](http://poser.pugx.org/ergebnis/json-pointer/d/monthly)](https://packagist.org/packages/ergebnis/json-pointer) 13 | 14 | This project provides a [`composer`](https://getcomposer.org) package with an abstraction of a [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). 15 | 16 | ## Installation 17 | 18 | Run 19 | 20 | ```sh 21 | composer require ergebnis/json-pointer 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### `ReferenceToken` 27 | 28 | You can create a `ReferenceToken` from a `string` value: 29 | 30 | ```php 31 | toJsonString(); // 'foo~19000~😆' 40 | $referenceToken->toString(); // 'foo/9000/😆' 41 | $referenceToken->toUriFragmentIdentifierString(); // 'foo~19000~1%F0%9F%98%86' 42 | ``` 43 | 44 | You can create a `ReferenceToken` from a [JSON `string` value](https://datatracker.ietf.org/doc/html/rfc6901#section-5): 45 | 46 | ```php 47 | 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 fragment identifier `string` value](https://datatracker.ietf.org/doc/html/rfc6901#section-6): 61 | 62 | ```php 63 | toJsonString(); // 'foo~19000~😆' 72 | $referenceToken->toString(); // 'foo/9000/😆' 73 | $referenceToken->toUriFragmentIdentifierString(); // 'foo~19000~1%F0%9F%98%86' 74 | ``` 75 | 76 | You can create a `ReferenceToken` from an `int` value: 77 | 78 | ```php 79 | toJsonString(); // '9001' 88 | $referenceToken->toString(); // '9001' 89 | $referenceToken->toUriFragmentIdentifierString(); // '9001' 90 | ``` 91 | 92 | You can compare `ReferenceToken`s: 93 | 94 | ```php 95 | equals($two); // true 106 | $one->equals($three); // false 107 | ``` 108 | 109 | ### `JsonPointer` 110 | 111 | You can create a `JsonPointer` referencing a document: 112 | 113 | ```php 114 | toJsonString(); // '' 123 | $jsonPointer->toUriFragmentIdentifierString(); // '#' 124 | ``` 125 | 126 | You can create a `JsonPointer` from a [JSON `string` representation](https://datatracker.ietf.org/doc/html/rfc6901#section-5) value: 127 | 128 | ```php 129 | toJsonString(); // '/foo/bar/😆' 138 | $jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar/%F0%9F%98%86' 139 | ``` 140 | 141 | You can create a `JsonPointer` from a [URI fragment identifier `string` representation](https://datatracker.ietf.org/doc/html/rfc6901#section-6) value: 142 | 143 | ```php 144 | toJsonString(); // '/foo/bar/😆' 153 | $jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar/%F0%9F%98%86' 154 | ``` 155 | 156 | You can create a `JsonPointer` from `ReferenceToken`s: 157 | 158 | ```php 159 | toJsonString(); // '/foo/bar' 173 | $jsonPointer->toUriFragmentIdentifierString(); // '#/foo/bar' 174 | ``` 175 | 176 | You can compare `JsonPointer`s: 177 | 178 | ```php 179 | equals($two); // false 190 | $one->equals($three); // true 191 | ``` 192 | 193 | You can append a `ReferenceToken` to a `JsonPointer`: 194 | 195 | ```php 196 | append($referenceToken); 207 | 208 | $newJsonPointer->toJsonString(); // '/foo/bar/baz' 209 | $newJsonPointer->toUriFragmentIdentifierString(); // '#foo/bar/baz' 210 | ``` 211 | 212 | ### `Specification` 213 | 214 | You can create a `Specification` that is always satisfied by a `JsonPointer`: 215 | 216 | ```php 217 | isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo')); // true 226 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/bar')); // true 227 | ``` 228 | 229 | You can create a `Specification` that is satisfied when a closure returns `true` for a `JsonPointer`: 230 | 231 | ```php 232 | toJsonString() === '/foo/bar'; 240 | }); 241 | 242 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo')); // false 243 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/bar')); // true 244 | ``` 245 | 246 | You can create a `Specification` that is satisfied when a `JsonPointer` equals another `JsonPointer`: 247 | 248 | ```php 249 | isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo')); // false 258 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/bar')); // true 259 | ``` 260 | 261 | You can create a `Specification` that is never satisfied by a `JsonPointer`: 262 | 263 | ```php 264 | isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo')); // false 273 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/bar')); // false 274 | ``` 275 | 276 | You can create a `Specification` that is satisfied when another `Specification` is not satisfied by a `JsonPointer`: 277 | 278 | ```php 279 | isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo')); // true 288 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/bar')); // false 289 | ``` 290 | 291 | You can compose `Specification`s to find out if a `JsonPointer` satisfies any of them: 292 | 293 | ```php 294 | toJsonString() === '/foo/bar'; 303 | }), 304 | Pointer\Specification::equals(Pointer\JsonPointer::fromJsonString('/foo/baz')), 305 | Pointer\Specification::never(), 306 | ); 307 | 308 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo')); // false 309 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/bar')); // true 310 | $specification->isSatisfiedBy(Pointer\JsonPointer::fromJsonString('/foo/baz')); // true 311 | ``` 312 | 313 | ## Changelog 314 | 315 | The maintainers of this project record notable changes to this project in a [changelog](CHANGELOG.md). 316 | 317 | ## Contributing 318 | 319 | The maintainers of this project suggest following the [contribution guide](.github/CONTRIBUTING.md). 320 | 321 | ## Code of Conduct 322 | 323 | The maintainers of this project ask contributors to follow the [code of conduct](https://github.com/ergebnis/.github/blob/main/CODE_OF_CONDUCT.md). 324 | 325 | ## General Support Policy 326 | 327 | The maintainers of this project provide limited support. 328 | 329 | You can support the maintenance of this project by [sponsoring @localheinz](https://github.com/sponsors/localheinz) or [requesting an invoice for services related to this project](mailto:am@localheinz.com?subject=ergebnis/json-pointer:%20Requesting%20invoice%20for%20services). 330 | 331 | ## PHP Version Support Policy 332 | 333 | This project supports PHP versions with [active and security support](https://www.php.net/supported-versions.php). 334 | 335 | The maintainers of this project add support for a PHP version following its initial release and drop support for a PHP version when it has reached the end of security support. 336 | 337 | ## Security Policy 338 | 339 | This project has a [security policy](.github/SECURITY.md). 340 | 341 | ## License 342 | 343 | This project uses the [MIT license](LICENSE.md). 344 | 345 | ## Social 346 | 347 | Follow [@localheinz](https://twitter.com/intent/follow?screen_name=localheinz) and [@ergebnis](https://twitter.com/intent/follow?screen_name=ergebnis) on Twitter. 348 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ergebnis/json-pointer", 3 | "description": "Provides an abstraction of a JSON pointer.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "json", 8 | "pointer", 9 | "rfc6901" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Andreas Möller", 14 | "email": "am@localheinz.com", 15 | "homepage": "https://localheinz.com" 16 | } 17 | ], 18 | "homepage": "https://github.com/ergebnis/json-pointer", 19 | "support": { 20 | "issues": "https://github.com/ergebnis/json-pointer/issues", 21 | "source": "https://github.com/ergebnis/json-pointer", 22 | "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md" 23 | }, 24 | "require": { 25 | "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" 26 | }, 27 | "require-dev": { 28 | "ergebnis/composer-normalize": "^2.43.0", 29 | "ergebnis/data-provider": "^3.2.0", 30 | "ergebnis/license": "^2.4.0", 31 | "ergebnis/php-cs-fixer-config": "^6.32.0", 32 | "ergebnis/phpunit-slow-test-detector": "^2.15.0", 33 | "fakerphp/faker": "^1.23.1", 34 | "infection/infection": "~0.26.6", 35 | "phpstan/extension-installer": "^1.4.3", 36 | "phpstan/phpstan": "^1.12.10", 37 | "phpstan/phpstan-deprecation-rules": "^1.2.1", 38 | "phpstan/phpstan-phpunit": "^1.4.0", 39 | "phpstan/phpstan-strict-rules": "^1.6.1", 40 | "phpunit/phpunit": "^9.6.19", 41 | "rector/rector": "^1.2.10" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Ergebnis\\Json\\Pointer\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Ergebnis\\Json\\Pointer\\Test\\": "test/" 51 | } 52 | }, 53 | "config": { 54 | "allow-plugins": { 55 | "composer/package-versions-deprecated": true, 56 | "ergebnis/composer-normalize": true, 57 | "infection/extension-installer": true, 58 | "phpstan/extension-installer": true 59 | }, 60 | "audit": { 61 | "abandoned": "report" 62 | }, 63 | "platform": { 64 | "php": "7.4.33" 65 | }, 66 | "preferred-install": "dist", 67 | "sort-packages": true 68 | }, 69 | "extra": { 70 | "branch-alias": { 71 | "dev-main": "3.6-dev" 72 | }, 73 | "composer-normalize": { 74 | "indent-size": 2, 75 | "indent-style": "space" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $referenceTokens; 27 | 28 | private function __construct(ReferenceToken ...$referenceTokens) 29 | { 30 | $this->referenceTokens = $referenceTokens; 31 | } 32 | 33 | /** 34 | * @see https://datatracker.ietf.org/doc/html/rfc6901#section-3 35 | * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 36 | * 37 | * @throws Exception\InvalidJsonPointer 38 | */ 39 | public static function fromJsonString(string $value): self 40 | { 41 | if (1 !== \preg_match(Pattern::JSON_STRING_JSON_POINTER, $value)) { 42 | throw Exception\InvalidJsonPointer::fromJsonString($value); 43 | } 44 | 45 | $jsonStringValues = \array_slice( 46 | \explode('/', $value), 47 | 1, 48 | ); 49 | 50 | return new self(...\array_map(static function (string $jsonStringValue): ReferenceToken { 51 | return ReferenceToken::fromJsonString($jsonStringValue); 52 | }, $jsonStringValues)); 53 | } 54 | 55 | public static function fromReferenceTokens(ReferenceToken ...$referenceTokens): self 56 | { 57 | return new self(...$referenceTokens); 58 | } 59 | 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 | 83 | public static function document(): self 84 | { 85 | return new self(); 86 | } 87 | 88 | public function append(ReferenceToken $referenceToken): self 89 | { 90 | $referenceTokens = $this->referenceTokens; 91 | 92 | $referenceTokens[] = $referenceToken; 93 | 94 | return new self(...$referenceTokens); 95 | } 96 | 97 | public function toJsonString(): string 98 | { 99 | if ([] === $this->referenceTokens) { 100 | return ''; 101 | } 102 | 103 | return \sprintf( 104 | '/%s', 105 | \implode('/', \array_map(static function (ReferenceToken $referenceToken): string { 106 | return $referenceToken->toJsonString(); 107 | }, $this->referenceTokens)), 108 | ); 109 | } 110 | 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 | 125 | /** 126 | * @return array 127 | */ 128 | public function toReferenceTokens(): array 129 | { 130 | return $this->referenceTokens; 131 | } 132 | 133 | public function equals(self $other): bool 134 | { 135 | return $this->toJsonString() === $other->toJsonString(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Pattern.php: -------------------------------------------------------------------------------- 1 | (\/(?P((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[01]))*))*)$/u'; 25 | 26 | /** 27 | * @see https://datatracker.ietf.org/doc/html/rfc6901#section-3 28 | */ 29 | public const JSON_STRING_REFERENCE_TOKEN = '/^(?P((?P[\x00-\x2E]|[\x30-\x7D]|[\x7F-\x{10FFFF}])|(?P~[01]))*)$/u'; 30 | public const URI_FRAGMENT_IDENTIFIER_JSON_POINTER = '/^(?P#(\/(?P((?P((?P((?P[a-zA-Z])|(?P\d)|-|\.|_|~))|(?P%(?P[0-9a-fA-F]){2})|(?P(!|\$|&|\'|\(|\)|\*|\+|,|;|=))|:|@))*)))*)$/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((?P((?P((?P[a-zA-Z])|(?P\d)|-|\.|_|~))|(?P%(?P[0-9a-fA-F]){2})|(?P(!|\$|&|\'|\(|\)|\*|\+|,|;|=))|:|@))*))$/u'; 40 | } 41 | -------------------------------------------------------------------------------- /src/ReferenceToken.php: -------------------------------------------------------------------------------- 1 | value = $value; 28 | } 29 | 30 | /** 31 | * @throws Exception\InvalidReferenceToken 32 | */ 33 | public static function fromInt(int $value): self 34 | { 35 | if (0 > $value) { 36 | throw Exception\InvalidReferenceToken::fromInt($value); 37 | } 38 | 39 | return new self((string) $value); 40 | } 41 | 42 | /** 43 | * @see https://datatracker.ietf.org/doc/html/rfc6901#section-5 44 | * 45 | * @throws Exception\InvalidReferenceToken 46 | */ 47 | public static function fromJsonString(string $value): self 48 | { 49 | if (1 !== \preg_match(Pattern::JSON_STRING_REFERENCE_TOKEN, $value)) { 50 | throw Exception\InvalidReferenceToken::fromJsonString($value); 51 | } 52 | 53 | return new self(\str_replace( 54 | [ 55 | '~1', 56 | '~0', 57 | ], 58 | [ 59 | '/', 60 | '~', 61 | ], 62 | $value, 63 | )); 64 | } 65 | 66 | public static function fromString(string $value): self 67 | { 68 | return new self($value); 69 | } 70 | 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 | 93 | public function toJsonString(): string 94 | { 95 | return \str_replace( 96 | [ 97 | '~', 98 | '/', 99 | ], 100 | [ 101 | '~0', 102 | '~1', 103 | ], 104 | $this->value, 105 | ); 106 | } 107 | 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 | 123 | public function toString(): string 124 | { 125 | return $this->value; 126 | } 127 | 128 | public function equals(self $other): bool 129 | { 130 | return $this->value === $other->value; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Specification.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 26 | } 27 | 28 | public function isSatisfiedBy(JsonPointer $jsonPointer): bool 29 | { 30 | $closure = $this->closure; 31 | 32 | return $closure($jsonPointer); 33 | } 34 | 35 | public static function always(): self 36 | { 37 | return new self(static function (): bool { 38 | return true; 39 | }); 40 | } 41 | 42 | public static function anyOf(self ...$specifications): self 43 | { 44 | return new self(static function (JsonPointer $jsonPointer) use ($specifications): bool { 45 | foreach ($specifications as $specification) { 46 | if ($specification->isSatisfiedBy($jsonPointer)) { 47 | return true; 48 | } 49 | } 50 | 51 | return false; 52 | }); 53 | } 54 | 55 | /** 56 | * @param \Closure(JsonPointer):bool $closure 57 | */ 58 | public static function closure(\Closure $closure): self 59 | { 60 | return new self(static function (JsonPointer $jsonPointer) use ($closure): bool { 61 | return true === $closure($jsonPointer); 62 | }); 63 | } 64 | 65 | public static function equals(JsonPointer $other): self 66 | { 67 | return new self(static function (JsonPointer $jsonPointer) use ($other): bool { 68 | return $jsonPointer->equals($other); 69 | }); 70 | } 71 | 72 | public static function never(): self 73 | { 74 | return new self(static function (): bool { 75 | return false; 76 | }); 77 | } 78 | 79 | public static function not(self $specification): self 80 | { 81 | return new self(static function (JsonPointer $jsonPointer) use ($specification): bool { 82 | return !$specification->isSatisfiedBy($jsonPointer); 83 | }); 84 | } 85 | } 86 | --------------------------------------------------------------------------------