Issues (4)

src/Pointer.php (2 issues)

1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Jom;
4
5
use DaveRandom\Jom\Exceptions\InvalidPointerException;
6
7
final class Pointer
8
{
9
    /** @var Pointer */
10
    private static $emptyPointer;
11
12
    /** @var string[] */
13
    private $path = [];
14
15
    /** @var int|null */
16
    private $relativeLevels = null;
17
18
    /** @var bool */
19
    private $keyLookup = false;
20
21
    /** @var string */
22
    private $string;
23
24
    /** @uses __init() */
25 1
    private static function __init(): void
26
    {
27 1
        self::$emptyPointer = new self();
28
    }
29
30
    /**
31
     * @throws InvalidPointerException
32
     */
33 34
    private static function decodePath(string $path): array
34
    {
35 34
        if ($path === '') {
36 14
            return [];
37
        }
38
39 20
        if ($path[0] !== '/') {
40
            throw new InvalidPointerException('JSON pointer path must be the empty string or begin with /');
41
        }
42
43 20
        $result = [];
44
45 20
        foreach (\explode('/', \substr($path, 1)) as $component) {
46 20
            $result[] = \str_replace(['~1', '~0'], ['/', '~'], $component);
47
        }
48
49 20
        return $result;
50
    }
51
52 22
    private static function encodePath(array $path): string
53
    {
54 22
        $result = '';
55
56 22
        foreach ($path as $component) {
57 14
            $result .= '/' . \str_replace(['~', '/'], ['~0', '~1'], $component);
58
        }
59
60 22
        return $result;
61
    }
62
63 48
    private static function splitRelativePointerComponents(string $pointer): array
64
    {
65 48
        return \preg_match('/^(0|[1-9][0-9]*)($|[^0-9].*)/i', $pointer, $match)
66 42
            ? [$match[2], (int)$match[1]]
67 48
            : [$pointer, null];
68
    }
69
70
    /**
71
     * @param string[] $path
72
     * @throws InvalidPointerException
73
     */
74 10
    private static function validatePointerComponents(array $path, ?int $relativeLevels, ?bool $isKeyLookup): void
75
    {
76 10
        if ($relativeLevels < 0) {
77
            throw new InvalidPointerException('Relative levels cannot be negative');
78
        }
79
80 10
        if ($isKeyLookup && !empty($path)) {
81
            throw new InvalidPointerException('Key lookup is invalid with non-empty path');
82
        }
83
84 10
        if ($isKeyLookup && $relativeLevels === null) {
85
            throw new InvalidPointerException('Key lookup is invalid for absolute pointers');
86
        }
87
    }
88
89
    /**
90
     * @throws InvalidPointerException
91
     */
92
    private function resolveAncestor(int $levels): Pointer
93
    {
94
        $result = clone $this;
95
        $result->string = null;
96
97
        if ($levels === 0) {
98
            return $result;
99
        }
100
101
        $count = \count($this->path) - $levels;
102
103
        if ($count < 0) {
104
            if ($this->relativeLevels === null) {
105
                throw new InvalidPointerException('Cannot reference ancestors above root of absolute pointer');
106
            }
107
108
            $result->relativeLevels = $this->relativeLevels - $count;
109
        }
110
111
        $result->path = $count > 0
112
            ? \array_slice($this->path, 0, $count)
113
            : [];
114
115
        return $result;
116
    }
117
118
    private function __construct() { }
119
120
    public static function empty(): Pointer
121
    {
122
        return self::$emptyPointer;
123
    }
124
125
    /**
126
     * @throws InvalidPointerException
127
     */
128 49
    public static function createFromString(string $pointer): Pointer
129
    {
130 49
        if ($pointer === '') {
131 1
            return self::$emptyPointer;
132
        }
133
134 48
        $result = new self();
135
136 48
        [$path, $result->relativeLevels] = self::splitRelativePointerComponents($pointer);
137
138 48
        $result->keyLookup = $result->relativeLevels !== null && $path === '#';
139
140 48
        if (!$result->keyLookup) {
141 34
            $result->path = self::decodePath($path);
142
        }
143
144 48
        return $result;
145
    }
146
147
    /**
148
     * @param string[] $path
149
     * @throws InvalidPointerException
150
     */
151 11
    public static function createFromParameters(array $path, ?int $relativeLevels = null, ?bool $isKeyLookup = false): Pointer
152
    {
153 11
        if ($path === [] && $relativeLevels === null && !$isKeyLookup) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isKeyLookup of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
154 1
            return self::$emptyPointer;
155
        }
156
157 10
        self::validatePointerComponents($path, $relativeLevels, $isKeyLookup);
158
159 10
        $result = new self();
160
161 10
        $result->relativeLevels = $relativeLevels;
162 10
        $result->keyLookup = $isKeyLookup ?? false;
163
164 10
        foreach ($path as $component) {
165 7
            $result->path[] = (string)$component;
166
        }
167
168 10
        return $result;
169
    }
170
171 37
    public function getPath(): array
172
    {
173 37
        return $this->path;
174
    }
175
176 42
    public function getRelativeLevels(): ?int
177
    {
178 42
        return $this->relativeLevels;
179
    }
180
181 49
    public function isRelative(): bool
182
    {
183 49
        return $this->relativeLevels !== null;
184
    }
185
186 30
    public function isKeyLookup(): bool
187
    {
188 30
        return $this->keyLookup;
189
    }
190
191
    /**
192
     * Resolve another pointer using this instance as a base and return the resulting pointer.
193
     *
194
     * If the reference pointer is absolute, it is returned unmodified.
195
     *
196
     * If the reference pointer is relative, it is used to generate a pointer that resolves to the same target node when
197
     * starting from location on which the current pointer is based. The result will be relative if the current pointer
198
     * is relative. The result will be a key lookup if the reference pointer is a key lookup.
199
     *
200
     * Examples:
201
     *
202
     * base:   /a/b/c
203
     * other:  1/d/e
204
     * result: /a/b/d/e
205
     *
206
     * base:   3/a/b/c
207
     * other:  4/d/e
208
     * result: 2/d/e
209
     *
210
     * @param Pointer|string $other
211
     * @throws InvalidPointerException
212
     */
213
    public function resolvePointer($other): Pointer
214
    {
215
        if (!($other instanceof self)) {
216
            $other = self::createFromString((string)$other);
217
        }
218
219
        if ($other->relativeLevels === null) {
220
            return $other;
221
        }
222
223
        $result = $this->resolveAncestor($other->relativeLevels);
224
225
        if (!empty($other->path)) {
226
            \array_push($result->path, ...$other->path);
227
        }
228
229
        $result->keyLookup = $other->keyLookup;
230
231
        if ($result->relativeLevels === $this->relativeLevels
232
            && $result->keyLookup === $this->keyLookup
233
            && $result->path !== $this->path) {
234
            return $this;
235
        }
236
237
        $result->string = null;
238
239
        return $result;
240
    }
241
242
    public function getPointerForChild($key, ...$keys): Pointer
243
    {
244
        $result = clone $this;
245
246
        $result->string = null;
247
        $result->keyLookup = false;
248
249
        \array_push($result->path, (string)$key, ...\array_map('strval', $keys));
250
251
        return $result;
252
    }
253
254
    /**
255
     * @throws InvalidPointerException
256
     */
257
    public function getPointerForAncestor(int $levels = 1): Pointer
258
    {
259
        if ($levels < 1) {
260
            throw new InvalidPointerException("Ancestor levels must be positive");
261
        }
262
263
        return self::resolveAncestor($levels);
0 ignored issues
show
Bug Best Practice introduced by
The method DaveRandom\Jom\Pointer::resolveAncestor() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

263
        return self::/** @scrutinizer ignore-call */ resolveAncestor($levels);
Loading history...
264
    }
265
266 26
    public function __toString(): string
267
    {
268 26
        if (isset($this->string)) {
269
            return $this->string;
270
        }
271
272 26
        $this->string = '';
273
274 26
        if ($this->relativeLevels !== null) {
275 19
            $this->string .= $this->relativeLevels;
276
        }
277
278 26
        $this->string .= $this->keyLookup
279 4
            ? '#'
280 22
            : self::encodePath($this->path);
281
282 26
        return $this->string;
283
    }
284
}
285
286
\DaveRandom\Jom\initialize(Pointer::class);
287