Completed
Push — master ( 3a4cf1...1221e0 )
by Chris
03:11
created

Document::evaluatePointer()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 12
cp 0
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 6
nop 2
crap 20
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Jom;
4
5
use DaveRandom\Jom\Exceptions\DocumentTreeCreationFailedException;
6
use DaveRandom\Jom\Exceptions\InvalidNodeValueException;
7
use DaveRandom\Jom\Exceptions\InvalidPointerException;
8
use DaveRandom\Jom\Exceptions\InvalidSubjectNodeException;
9
use DaveRandom\Jom\Exceptions\ParseFailureException;
10
use DaveRandom\Jom\Exceptions\PointerReferenceNotFoundException;
11
use DaveRandom\Jom\Exceptions\WriteOperationForbiddenException;
12
use ExceptionalJSON\DecodeErrorException;
13
14
final class Document implements \JsonSerializable
15
{
16
    /** @var Node */
17
    private $rootNode;
18
19
    private const SCALAR_VALUE_NODE_CLASSES = [
20
        'boolean' => BooleanNode::class,
21
        'integer' => NumberNode::class,
22
        'double' => NumberNode::class,
23
        'string' => StringNode::class,
24
    ];
25
26
    private static function createScalarOrNullNodeFromValue(Document $doc, $value): ?Node
27
    {
28
        if ($value === null) {
29
            return new NullNode($doc);
30
        }
31
32
        $className = self::SCALAR_VALUE_NODE_CLASSES[\gettype($value)] ?? null;
33
34
        if ($className !== null) {
35
            return new $className($doc, $value);
36
        }
37
38
        return null;
39
    }
40
41
    /**
42
     * @throws WriteOperationForbiddenException
43
     * @throws InvalidNodeValueException
44
     * @throws InvalidSubjectNodeException
45
     */
46
    private static function createArrayNodeFromSafePackedArray(Document $doc, array $values): ArrayNode
47
    {
48
        $node = new ArrayNode($doc);
49
50
        foreach ($values as $value) {
51
            $node->push(self::createNodeFromSafeValue($doc, $value));
52
        }
53
54
        return $node;
55
    }
56
57
    /**
58
     * @throws WriteOperationForbiddenException
59
     * @throws InvalidNodeValueException
60
     * @throws InvalidSubjectNodeException
61
     */
62
    private static function createArrayNodeFromUnsafePackedArray(Document $doc, array $values): ArrayNode
63
    {
64
        $node = new ArrayNode($doc);
65
66
        foreach ($values as $value) {
67
            $node->push(self::createNodeFromUnsafeValue($doc, $value));
68
        }
69
70
        return $node;
71
    }
72
73
    /**
74
     * @throws WriteOperationForbiddenException
75
     * @throws InvalidNodeValueException
76
     * @throws InvalidSubjectNodeException
77
     */
78
    private static function createObjectNodeFromSafeStdClass(Document $doc, \stdClass $values): ObjectNode
79
    {
80
        $node = new ObjectNode($doc);
81
82
        foreach ($values as $key => $value) {
83
            $node->setProperty($key, self::createNodeFromSafeValue($doc, $value));
84
        }
85
86
        return $node;
87
    }
88
89
    /**
90
     * @throws WriteOperationForbiddenException
91
     * @throws InvalidNodeValueException
92
     * @throws InvalidSubjectNodeException
93
     */
94
    private static function createObjectNodeFromUnsafeStdClass(Document $doc, \stdClass $values): ObjectNode
95
    {
96
        $node = new ObjectNode($doc);
97
98
        foreach ($values as $key => $value) {
99
            $node->setProperty($key, self::createNodeFromUnsafeValue($doc, $value));
100
        }
101
102
        return $node;
103
    }
104
105
    /**
106
     * @throws WriteOperationForbiddenException
107
     * @throws InvalidNodeValueException
108
     * @throws InvalidSubjectNodeException
109
     */
110
    private static function createObjectNodeFromUnsafePropertyArray(Document $doc, array $properties): ObjectNode
111
    {
112
        $node = new ObjectNode($doc);
113
114
        foreach ($properties as $name => $value) {
115
            $node->setProperty($name, self::createNodeFromUnsafeValue($doc, $value));
116
        }
117
118
        return $node;
119
    }
120
121
    /**
122
     * @throws WriteOperationForbiddenException
123
     * @throws InvalidNodeValueException
124
     * @throws InvalidSubjectNodeException
125
     */
126
    private static function createVectorNodeFromUnsafeArray(Document $doc, array $values): VectorNode
127
    {
128
        $i = 0;
129
        $packed = true;
130
131
        foreach ($values as $key => $value) {
132
            if ($key !== $i++) {
133
                $packed = false;
134
                break;
135
            }
136
        }
137
138
        return $packed
139
            ? self::createArrayNodeFromUnsafePackedArray($doc, $values)
140
            : self::createObjectNodeFromUnsafePropertyArray($doc, $values);
141
    }
142
143
    /**
144
     * @throws WriteOperationForbiddenException
145
     * @throws InvalidNodeValueException
146
     * @throws InvalidSubjectNodeException
147
     */
148
    private static function createNodeFromUnsafeObject(Document $doc, object $object): Node
149
    {
150
        if ($object instanceof \stdClass) {
151
            return self::createObjectNodeFromUnsafeStdClass($doc, $object);
152
        }
153
154
        if ($object instanceof \JsonSerializable) {
155
            return self::createNodeFromUnsafeValue($doc, $object->jsonSerialize());
156
        }
157
158
        return self::createObjectNodeFromUnsafePropertyArray($doc, \get_object_vars($object));
159
    }
160
161
    /**
162
     * @throws WriteOperationForbiddenException
163
     * @throws InvalidNodeValueException
164
     * @throws InvalidSubjectNodeException
165
     */
166
    private static function createNodeFromSafeValue(Document $doc, $value): Node
167
    {
168
        if (null !== $node = self::createScalarOrNullNodeFromValue($doc, $value)) {
169
            return $node;
170
        }
171
172
        if (\is_object($value)) {
173
            return self::createObjectNodeFromSafeStdClass($doc, $value);
174
        }
175
176
        if (\is_array($value)) {
177
            return self::createArrayNodeFromSafePackedArray($doc, $value);
178
        }
179
180
        throw new InvalidNodeValueException("Failed to create node from value of type '" . \gettype($value) . "'");
181
    }
182
183
    /**
184
     * @throws WriteOperationForbiddenException
185
     * @throws InvalidNodeValueException
186
     * @throws InvalidSubjectNodeException
187
     */
188
    private static function createNodeFromUnsafeValue(Document $doc, $value): Node
189
    {
190
        if (null !== $node = self::createScalarOrNullNodeFromValue($doc, $value)) {
191
            return $node;
192
        }
193
194
        if (\is_object($value)) {
195
            return self::createNodeFromUnsafeObject($doc, $value);
196
        }
197
198
        if (\is_array($value)) {
199
            return self::createVectorNodeFromUnsafeArray($doc, $value);
200
        }
201
202
        throw new InvalidNodeValueException("Failed to create node from value of type '" . \gettype($value) . "'");
203
    }
204
205
    /**
206
     * @throws PointerReferenceNotFoundException
207
     */
208
    private function evaluatePointerPath(Pointer $pointer, Node $current): Node
209
    {
210
        foreach ($pointer->getPath() as $component) {
211
            if (!($current instanceof VectorNode)) {
212
                throw new PointerReferenceNotFoundException(
213
                    "Pointer '{$pointer}' does not indicate a valid path in the document"
214
                );
215
            }
216
217
            if (!$current->offsetExists($component)) {
218
                throw new PointerReferenceNotFoundException("The referenced property or index '{$component}' does not exist");
219
            }
220
221
            $current = $current->offsetGet($component);
222
        }
223
224
        return $current;
225
    }
226
227
    /**
228
     * @throws PointerReferenceNotFoundException
229
     * @throws InvalidSubjectNodeException
230
     */
231
    private function evaluateRelativePointer(Pointer $pointer, Node $current): Node
232
    {
233
        if ($current->getOwnerDocument() !== $this) {
234
            throw new InvalidSubjectNodeException('Base node belongs to a different document');
235
        }
236
237
        for ($i = $pointer->getRelativeLevels(); $i > 0; $i--) {
238
            $current = $current->getParent();
239
240
            if ($current === null) {
241
                throw new PointerReferenceNotFoundException(
242
                    "Pointer '{$pointer}' does not indicate a valid path in the document relative to the supplied node"
243
                );
244
            }
245
        }
246
247
        return $this->evaluatePointerPath($pointer, $current);
248
    }
249
250
    /**
251
     * @throws InvalidNodeValueException
252
     * @throws InvalidSubjectNodeException
253
     * @throws WriteOperationForbiddenException
254
     */
255
    private function importVectorNode(VectorNode $node): VectorNode
256
    {
257
        if (!($node instanceof ArrayNode || $node instanceof ObjectNode)) {
258
            throw new InvalidSubjectNodeException('Source node is of unknown type ' . \get_class($node));
259
        }
260
261
        $newNode = new $node($this);
262
263
        foreach ($node as $key => $value) {
264
            $newNode[$key] = $this->import($value);
265
        }
266
267
        return $newNode;
268
    }
269
270
    /**
271
     * @throws InvalidSubjectNodeException
272
     */
273
    private function importScalarNode(Node $node): Node
274
    {
275
        if (!($node instanceof BooleanNode || $node instanceof NumberNode || $node instanceof StringNode)) {
276
            throw new InvalidSubjectNodeException('Source node is of unknown type ' . \get_class($node));
277
        }
278
279
        return new $node($this, $node->getValue());
280
    }
281
282
    /**
283
     * @throws DocumentTreeCreationFailedException
284
     * @throws ParseFailureException
285
     */
286
    public static function parse(string $json, int $depthLimit = 512, int $options = 0): Document
287
    {
288
        try {
289
            $data = \ExceptionalJSON\decode($json, false, $depthLimit, $options & ~\JSON_OBJECT_AS_ARRAY);
290
291
            $doc = new self();
292
            $doc->rootNode = self::createNodeFromSafeValue($doc, $data);
293
294
            return $doc;
295
        } catch (DecodeErrorException $e) {
296
            throw new ParseFailureException("Decoding JSON string failed: {$e->getMessage()}", $e);
297
        } catch (InvalidNodeValueException $e) {
298
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
299
        } catch (\Throwable $e) {
300
            throw new DocumentTreeCreationFailedException("Unexpected error: {$e->getMessage()}", $e);
301
        }
302
    }
303
304
    /**
305
     * @throws DocumentTreeCreationFailedException
306
     */
307
    public static function createFromData($data): Document
308
    {
309
        try {
310
            $doc = new self();
311
            $doc->rootNode = self::createNodeFromUnsafeValue($doc, $data);
312
313
            return $doc;
314
        } catch (InvalidNodeValueException $e) {
315
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
316
        } catch (\Throwable $e) {
317
            throw new DocumentTreeCreationFailedException("Unexpected error: {$e->getMessage()}", $e);
318
        }
319
    }
320
321
    public function getRootNode(): ?Node
322
    {
323
        return $this->rootNode;
324
    }
325
326
    /**
327
     * @throws InvalidSubjectNodeException
328
     * @throws WriteOperationForbiddenException
329
     * @throws InvalidNodeValueException
330
     * @throws InvalidSubjectNodeException
331
     */
332
    public function import(Node $node): Node
333
    {
334
        if ($node->getOwnerDocument() === $this) {
335
            throw new InvalidSubjectNodeException('Cannot import tne supplied node, already owned by this document');
336
        }
337
338
        if ($node instanceof NullNode) {
339
            return new NullNode($this);
340
        }
341
342
        return $node instanceof VectorNode
343
            ? $this->importVectorNode($node)
344
            : $this->importScalarNode($node);
345
    }
346
347
    /**
348
     * @param Pointer|string $pointer
349
     * @return Node|int|string
350
     * @throws InvalidPointerException
351
     * @throws PointerReferenceNotFoundException
352
     * @throws InvalidSubjectNodeException
353
     */
354
    public function evaluatePointer($pointer, Node $base = null)
355
    {
356
        if (!($pointer instanceof Pointer)) {
357
            $pointer = Pointer::createFromString((string)$pointer);
358
        }
359
360
        if (!$pointer->isRelative()) {
361
            return $this->evaluatePointerPath($pointer, $this->rootNode);
362
        }
363
364
        $target = $this->evaluateRelativePointer($pointer, $base ?? $this->rootNode);
365
366
        return $pointer->isKeyLookup()
367
            ? $target->getKey()
368
            : $target;
369
    }
370
371
    public function jsonSerialize()
372
    {
373
        return $this->rootNode !== null
374
            ? $this->rootNode->jsonSerialize()
375
            : null;
376
    }
377
}
378