Completed
Push — master ( 0ff49e...c91a90 )
by Chris
02:27
created

Pointer::getPointerForChild()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

241
        return self::/** @scrutinizer ignore-call */ resolveAncestor($levels);
Loading history...
242
    }
243
244 26
    public function __toString(): string
245
    {
246 26
        if (isset($this->string)) {
247
            return $this->string;
248
        }
249
250 26
        $this->string = '';
251
252 26
        if ($this->relativeLevels !== null) {
253 19
            $this->string .= $this->relativeLevels;
254
        }
255
256 26
        $this->string .= $this->keyLookup
257 4
            ? '#'
258 22
            : self::encodePath($this->path);
259
260 26
        return $this->string;
261
    }
262
}
263