410 lines
No EOL
9.9 KiB
PHP
410 lines
No EOL
9.9 KiB
PHP
<?php
|
|
/* ============================================================================
|
|
* Copyright 2020 Zindex Software
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
* ============================================================================ */
|
|
|
|
namespace Opis\JsonSchema;
|
|
|
|
final class JsonPointer
|
|
{
|
|
/** @var string */
|
|
protected const PATTERN = '~^(?:(?<level>0|[1-9][0-9]*)(?<shift>(?:\+|-)(?:0|[1-9][0-9]*))?)?(?<pointer>(?:/[^/#]*)*)(?<fragment>#)?$~';
|
|
|
|
/** @var string */
|
|
protected const UNESCAPED = '/~([^01]|$)/';
|
|
|
|
protected int $level = -1;
|
|
|
|
protected int $shift = 0;
|
|
|
|
protected bool $fragment = false;
|
|
|
|
/** @var string[]|int[] */
|
|
protected array $path;
|
|
|
|
protected ?string $str = null;
|
|
|
|
final protected function __construct(array $path, int $level = -1, int $shift = 0, bool $fragment = false)
|
|
{
|
|
$this->path = $path;
|
|
$this->level = $level < 0 ? -1 : $level;
|
|
$this->shift = $shift;
|
|
$this->fragment = $level >= 0 && $fragment;
|
|
}
|
|
|
|
public function isRelative(): bool
|
|
{
|
|
return $this->level >= 0;
|
|
}
|
|
|
|
public function isAbsolute(): bool
|
|
{
|
|
return $this->level < 0;
|
|
}
|
|
|
|
public function level(): int
|
|
{
|
|
return $this->level;
|
|
}
|
|
|
|
public function shift(): int
|
|
{
|
|
return $this->shift;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function path(): array
|
|
{
|
|
return $this->path;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function hasFragment(): bool
|
|
{
|
|
return $this->fragment;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function __toString(): string
|
|
{
|
|
if ($this->str === null) {
|
|
if ($this->level >= 0) {
|
|
$this->str = (string)$this->level;
|
|
|
|
if ($this->shift !== 0) {
|
|
if ($this->shift > 0) {
|
|
$this->str .= '+';
|
|
}
|
|
$this->str .= $this->shift;
|
|
}
|
|
|
|
if ($this->path) {
|
|
$this->str .= '/';
|
|
$this->str .= implode('/', self::encodePath($this->path));
|
|
}
|
|
|
|
if ($this->fragment) {
|
|
$this->str .= '#';
|
|
}
|
|
} else {
|
|
$this->str = '/';
|
|
$this->str .= implode('/', self::encodePath($this->path));
|
|
}
|
|
}
|
|
|
|
return $this->str;
|
|
}
|
|
|
|
/**
|
|
* @param $data
|
|
* @param array|null $path
|
|
* @param null $default
|
|
* @return mixed
|
|
*/
|
|
public function data($data, ?array $path = null, $default = null)
|
|
{
|
|
if ($this->level < 0) {
|
|
return self::getData($data, $this->path, false, $default);
|
|
}
|
|
|
|
if ($path !== null) {
|
|
$path = $this->absolutePath($path);
|
|
}
|
|
|
|
if ($path === null) {
|
|
return $default;
|
|
}
|
|
|
|
return self::getData($data, $path, $this->fragment, $default);
|
|
}
|
|
|
|
/**
|
|
* @param array $path
|
|
* @return array|null
|
|
*/
|
|
public function absolutePath(array $path = []): ?array
|
|
{
|
|
if ($this->level < 0) {
|
|
// Absolute pointer
|
|
return $this->path;
|
|
}
|
|
|
|
if ($this->level === 0) {
|
|
if ($this->shift && !$this->handleShift($path)) {
|
|
return null;
|
|
}
|
|
return $this->path ? array_merge($path, $this->path) : $path;
|
|
}
|
|
|
|
$count = count($path);
|
|
if ($count === $this->level) {
|
|
if ($this->shift) {
|
|
return null;
|
|
}
|
|
return $this->path;
|
|
}
|
|
|
|
if ($count > $this->level) {
|
|
$count -= $this->level;
|
|
|
|
/** @var array $path */
|
|
$path = array_slice($path, 0, $count);
|
|
|
|
if ($this->shift && !$this->handleShift($path, $count)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->path ? array_merge($path, $this->path) : $path;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function handleShift(array &$path, ?int $count = null): bool
|
|
{
|
|
if (!$path) {
|
|
return false;
|
|
}
|
|
|
|
$count ??= count($path);
|
|
|
|
$last = $path[$count - 1];
|
|
|
|
if (is_string($last) && preg_match('/^[1-9]\d*$/', $last)) {
|
|
$last = (int) $last;
|
|
}
|
|
|
|
if (!is_int($last)) {
|
|
return false;
|
|
}
|
|
|
|
$path[$count - 1] = $last + $this->shift;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static function parse(string $pointer, bool $decode = true): ?self
|
|
{
|
|
if ($pointer === '' || !preg_match(self::PATTERN, $pointer, $m)) {
|
|
// Not a pointer
|
|
return null;
|
|
}
|
|
|
|
$pointer = $m['pointer'] ?? null;
|
|
|
|
// Check if the pointer is escaped
|
|
if ($decode && $pointer && preg_match(self::UNESCAPED, $pointer)) {
|
|
// Invalid pointer
|
|
return null;
|
|
}
|
|
|
|
$level = isset($m['level']) && $m['level'] !== ''
|
|
? (int)$m['level']
|
|
: -1;
|
|
|
|
$shift = 0;
|
|
if ($level >= 0 && isset($m['shift']) && $m['shift'] !== '') {
|
|
$shift = (int) $m['shift'];
|
|
}
|
|
|
|
$fragment = isset($m['fragment']) && $m['fragment'] === '#';
|
|
unset($m);
|
|
|
|
if ($fragment && $level < 0) {
|
|
return null;
|
|
}
|
|
|
|
if ($pointer === '') {
|
|
$pointer = null;
|
|
} elseif ($pointer !== null) {
|
|
// Remove leading slash
|
|
$pointer = substr($pointer, 1);
|
|
|
|
if ($pointer !== '') {
|
|
$pointer = self::decodePath(explode('/', $pointer));
|
|
} else {
|
|
$pointer = null;
|
|
}
|
|
}
|
|
|
|
return new self($pointer ?? [], $level, $shift, $fragment);
|
|
}
|
|
|
|
/**
|
|
* @param $data
|
|
* @param array|null $path
|
|
* @param bool $fragment
|
|
* @param null $default
|
|
* @return mixed
|
|
*/
|
|
public static function getData($data, ?array $path = null, bool $fragment = false, $default = null)
|
|
{
|
|
if ($path === null) {
|
|
return $default;
|
|
}
|
|
|
|
if (!$path) {
|
|
return $fragment ? $default : $data;
|
|
}
|
|
|
|
if ($fragment) {
|
|
return end($path);
|
|
}
|
|
|
|
foreach ($path as $key) {
|
|
if (is_array($data)) {
|
|
if (!array_key_exists($key, $data)) {
|
|
return $default;
|
|
}
|
|
$data = $data[$key];
|
|
} elseif (is_object($data)) {
|
|
if (!property_exists($data, $key)) {
|
|
return $default;
|
|
}
|
|
$data = $data->{$key};
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @param string|string[] $path
|
|
* @return string|string[]
|
|
*/
|
|
public static function encodePath($path)
|
|
{
|
|
$path = str_replace('~', '~0', $path);
|
|
$path = str_replace('/', '~1', $path);
|
|
|
|
if (is_array($path)) {
|
|
return array_map('rawurlencode', $path);
|
|
}
|
|
|
|
return rawurlencode($path);
|
|
}
|
|
|
|
/**
|
|
* @param string|string[] $path
|
|
* @return string|string[]
|
|
*/
|
|
public static function decodePath($path)
|
|
{
|
|
if (is_array($path)) {
|
|
$path = array_map('rawurldecode', $path);
|
|
} else {
|
|
$path = rawurldecode($path);
|
|
}
|
|
|
|
$path = str_replace('~1', '/', $path);
|
|
$path = str_replace('~0', '~', $path);
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* @param array $path
|
|
* @return string
|
|
*/
|
|
public static function pathToString(array $path): string
|
|
{
|
|
if (!$path) {
|
|
return '/';
|
|
}
|
|
|
|
return '/' . implode('/', self::encodePath($path));
|
|
}
|
|
|
|
/**
|
|
* @param array $path
|
|
* @return string
|
|
*/
|
|
public static function pathToFragment(array $path): string
|
|
{
|
|
if (!$path) {
|
|
return '#';
|
|
}
|
|
|
|
return '#/' . implode('/', self::encodePath($path));
|
|
}
|
|
|
|
/**
|
|
* @param string $pointer
|
|
* @return bool
|
|
*/
|
|
public static function isAbsolutePointer(string $pointer): bool
|
|
{
|
|
if ($pointer === '/') {
|
|
return true;
|
|
}
|
|
|
|
if (!preg_match(self::PATTERN, $pointer, $m)) {
|
|
return false;
|
|
}
|
|
|
|
if (isset($m['fragment']) || isset($m['level']) && $m['level'] !== '') {
|
|
return false;
|
|
}
|
|
|
|
if (!isset($m['pointer']) || $m['pointer'] === '') {
|
|
return true;
|
|
}
|
|
|
|
return !preg_match(self::UNESCAPED, $m['pointer']);
|
|
}
|
|
|
|
/**
|
|
* @param string $pointer
|
|
* @return bool
|
|
*/
|
|
public static function isRelativePointer(string $pointer): bool
|
|
{
|
|
if ($pointer === '') {
|
|
return false;
|
|
}
|
|
|
|
if (!preg_match(self::PATTERN, $pointer, $m)) {
|
|
return false;
|
|
}
|
|
|
|
if (!isset($m['level']) || $m['level'] === '' || (int)$m['level'] < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!isset($m['pointer']) || $m['pointer'] === '') {
|
|
return true;
|
|
}
|
|
|
|
return !preg_match(self::UNESCAPED, $m['pointer']);
|
|
}
|
|
|
|
public static function createAbsolute(array $path): self
|
|
{
|
|
return new self($path, -1, 0, false);
|
|
}
|
|
|
|
public static function createRelative(int $level, array $path = [], int $shift = 0, bool $fragment = false): self
|
|
{
|
|
return new self($path, $level, $shift, $fragment);
|
|
}
|
|
} |