Completed
Push — master ( c91a90...e32b70 )
by Chris
02:54
created

Pointer::empty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Jom;
4
5
use DaveRandom\Jom\Exceptions\InvalidPointerException;
6
7
\DaveRandom\Jom\initialize(Pointer::class);
8
9
final class Pointer
10
{
11
    /** @var Pointer */
12
    private static $emptyPointer;
13
14
    /** @var string[] */
15
    private $path = [];
16
17
    /** @var int|null */
18
    private $relativeLevels = null;
19
20
    /** @var bool */
21
    private $keyLookup = false;
22
23
    /** @var string */
24 35
    private $string;
25
26 35
    /** @uses __init() */
27 15
    private static function __init(): void
28
    {
29
        self::$emptyPointer = new self();
30 20
    }
31
32
    /**
33
     * @throws InvalidPointerException
34 20
     */
35
    private static function decodePath(string $path): array
36 20
    {
37 20
        if ($path === '') {
38
            return [];
39
        }
40 20
41
        if ($path[0] !== '/') {
42
            throw new InvalidPointerException('JSON pointer path must be the empty string or begin with /');
43 22
        }
44
45 22
        $result = [];
46
47 22
        foreach (\explode('/', \substr($path, 1)) as $component) {
48 14
            $result[] = \str_replace(['~1', '~0'], ['/', '~'], $component);
49
        }
50
51 22
        return $result;
52
    }
53
54 49
    private static function encodePath(array $path): string
55
    {
56 49
        $result = '';
57 42
58 49
        foreach ($path as $component) {
59
            $result .= '/' . \str_replace(['~', '/'], ['~0', '~1'], $component);
60
        }
61
62
        return $result;
63
    }
64
65 11
    private static function splitRelativePointerComponents(string $pointer): array
66
    {
67 11
        return \preg_match('/^(0|[1-9][0-9]*)($|[^0-9].*)/i', $pointer, $match)
68
            ? [$match[2], (int)$match[1]]
69
            : [$pointer, null];
70
    }
71 11
72
    /**
73
     * @param string[] $path
74
     * @throws InvalidPointerException
75 11
     */
76
    private static function validatePointerComponents(array $path, ?int $relativeLevels, ?bool $isKeyLookup): void
77
    {
78
        if ($relativeLevels < 0) {
79
            throw new InvalidPointerException('Relative levels cannot be negative');
80
        }
81
82
        if ($isKeyLookup && !empty($path)) {
83
            throw new InvalidPointerException('Key lookup is invalid with non-empty path');
84
        }
85
86
        if ($isKeyLookup && $relativeLevels === null) {
87
            throw new InvalidPointerException('Key lookup is invalid for absolute pointers');
88
        }
89
    }
90
91
    /**
92
     * @throws InvalidPointerException
93
     */
94
    private function resolveAncestor(int $levels): Pointer
95
    {
96
        $result = clone $this;
97
        $result->string = null;
98
99
        if ($levels === 0) {
100
            return $result;
101
        }
102
103
        $count = \count($this->path) - $levels;
104
105
        if ($count < 0) {
106
            if ($this->relativeLevels === null) {
107
                throw new InvalidPointerException('Cannot reference ancestors above root of absolute pointer');
108
            }
109
110
            $result->relativeLevels = $this->relativeLevels - $count;
111
        }
112 49
113
        $result->path = $count > 0
114 49
            ? \array_slice($this->path, 0, $count)
115
            : [];
116 49
117
        return $result;
118 49
    }
119
120 49
    private function __construct() { }
121 35
122
    public static function empty(): Pointer
123
    {
124 49
        return self::$emptyPointer;
125
    }
126
127
    /**
128
     * @throws InvalidPointerException
129
     */
130
    public static function createFromString(string $pointer): Pointer
131
    {
132
        if ($pointer === '') {
133 11
            return self::$emptyPointer;
134
        }
135 11
136
        $result = new self();
137 11
138
        [$path, $result->relativeLevels] = self::splitRelativePointerComponents($pointer);
139 11
140 11
        $result->keyLookup = $result->relativeLevels !== null && $path === '#';
141
142 11
        if (!$result->keyLookup) {
143 7
            $result->path = self::decodePath($path);
144
        }
145
146 11
        return $result;
147
    }
148
149 37
    /**
150
     * @param string[] $path
151 37
     * @throws InvalidPointerException
152
     */
153
    public static function createFromParameters(array $path, ?int $relativeLevels = null, ?bool $isKeyLookup = false): Pointer
154 42
    {
155
        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...
156 42
            return self::$emptyPointer;
157
        }
158
159 49
        self::validatePointerComponents($path, $relativeLevels, $isKeyLookup);
160
161 49
        $result = new self();
162
163
        $result->relativeLevels = $relativeLevels;
164 30
        $result->keyLookup = $isKeyLookup ?? false;
165
166 30
        foreach ($path as $component) {
167
            $result->path[] = (string)$component;
168
        }
169
170
        return $result;
171
    }
172
173
    public function getPath(): array
174
    {
175
        return $this->path;
176
    }
177
178
    public function getRelativeLevels(): ?int
179
    {
180
        return $this->relativeLevels;
181
    }
182
183
    public function isRelative(): bool
184
    {
185
        return $this->relativeLevels !== null;
186
    }
187
188
    public function isKeyLookup(): bool
189
    {
190
        return $this->keyLookup;
191
    }
192
193
    /**
194
     * Resolve another pointer using this instance as a base and return the resulting pointer.
195
     *
196
     * If the reference pointer is absolute, it is returned unmodified.
197
     *
198
     * If the reference pointer is relative, it is used to generate a pointer that resolves to the same target node when
199
     * starting from location on which the current pointer is based. The result will be relative if the current pointer
200
     * is relative. The result will be a key lookup if the reference pointer is a key lookup.
201
     *
202
     * Examples:
203
     *
204
     * base:   /a/b/c
205
     * other:  1/d/e
206
     * result: /a/b/d/e
207
     *
208
     * base:   3/a/b/c
209
     * other:  4/d/e
210
     * result: 2/d/e
211
     *
212
     * @param Pointer|string $other
213
     * @throws InvalidPointerException
214
     */
215
    public function resolvePointer($other): Pointer
216
    {
217
        if (!($other instanceof self)) {
218
            $other = self::createFromString((string)$other);
219
        }
220
221
        if ($other->relativeLevels === null) {
222
            return $other;
223
        }
224
225
        $result = $this->resolveAncestor($other->relativeLevels);
226
227
        if (!empty($other->path)) {
228
            \array_push($result->path, ...$other->path);
229
        }
230
231
        $result->keyLookup = $other->keyLookup;
232
233
        if ($result->relativeLevels === $this->relativeLevels
234
            && $result->keyLookup === $this->keyLookup
235
            && $result->path !== $this->path) {
236
            return $this;
237
        }
238
239
        $result->string = null;
240
241
        return $result;
242
    }
243
244 26
    public function getPointerForChild($key, ...$keys): Pointer
245
    {
246 26
        $result = clone $this;
247
248
        $result->string = null;
249
        $result->keyLookup = false;
250 26
251
        \array_push($result->path, (string)$key, ...\array_map('strval', $keys));
252 26
253 19
        return $result;
254
    }
255
256 26
    /**
257 4
     * @throws InvalidPointerException
258 22
     */
259
    public function getPointerForAncestor(int $levels = 1): Pointer
260 26
    {
261
        if ($levels < 1) {
262
            throw new InvalidPointerException("Ancestor levels must be positive");
263
        }
264
265
        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

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