Passed
Push — master ( 37810d...c6a7a1 )
by Chris
03:03
created

Document::createArrayNodeFromSafePackedArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 7
cp 0
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
crap 6
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 static function getSafeNodeFactory(): SafeNodeFactory
20
    {
21
        static $factory;
22
        return $factory ?? $factory = new SafeNodeFactory();
23
    }
24
25
    private static function getUnsafeNodeFactory(): UnsafeNodeFactory
26
    {
27
        static $factory;
28
        return $factory ?? $factory = new UnsafeNodeFactory();
29
    }
30
31
    /**
32
     * @throws PointerReferenceNotFoundException
33
     */
34
    private function evaluatePointerPath(Pointer $pointer, Node $current): Node
35
    {
36
        foreach ($pointer->getPath() as $component) {
37
            if (!($current instanceof VectorNode)) {
38
                throw new PointerReferenceNotFoundException(
39
                    "Pointer '{$pointer}' does not indicate a valid path in the document"
40
                );
41
            }
42
43
            if (!$current->offsetExists($component)) {
44
                throw new PointerReferenceNotFoundException("The referenced property or index '{$component}' does not exist");
45
            }
46
47
            $current = $current->offsetGet($component);
48
        }
49
50
        return $current;
51
    }
52
53
    /**
54
     * @throws PointerReferenceNotFoundException
55
     * @throws InvalidSubjectNodeException
56
     */
57
    private function evaluateRelativePointer(Pointer $pointer, Node $current): Node
58
    {
59
        if ($current->getOwnerDocument() !== $this) {
60
            throw new InvalidSubjectNodeException('Base node belongs to a different document');
61
        }
62
63
        for ($i = $pointer->getRelativeLevels(); $i > 0; $i--) {
64
            $current = $current->getParent();
65
66
            if ($current === null) {
67
                throw new PointerReferenceNotFoundException(
68
                    "Pointer '{$pointer}' does not indicate a valid path in the document relative to the supplied node"
69
                );
70
            }
71
        }
72
73
        return $this->evaluatePointerPath($pointer, $current);
74
    }
75
76
    /**
77
     * @throws InvalidNodeValueException
78
     * @throws InvalidSubjectNodeException
79
     * @throws WriteOperationForbiddenException
80
     */
81
    private function importVectorNode(VectorNode $node): VectorNode
82
    {
83
        if (!($node instanceof ArrayNode || $node instanceof ObjectNode)) {
84
            throw new InvalidSubjectNodeException('Source node is of unknown type ' . \get_class($node));
85
        }
86
87
        $newNode = new $node($this);
88
89
        foreach ($node as $key => $value) {
90
            $newNode[$key] = $this->import($value);
91
        }
92
93
        return $newNode;
94
    }
95
96
    /**
97
     * @throws InvalidSubjectNodeException
98
     */
99
    private function importScalarNode(Node $node): Node
100
    {
101
        if (!($node instanceof BooleanNode || $node instanceof NumberNode || $node instanceof StringNode)) {
102
            throw new InvalidSubjectNodeException('Source node is of unknown type ' . \get_class($node));
103
        }
104
105
        return new $node($this, $node->getValue());
106
    }
107
108
    /**
109
     * @throws DocumentTreeCreationFailedException
110
     * @throws ParseFailureException
111
     */
112
    public static function parse(string $json, int $depthLimit = 512, int $options = 0): Document
113
    {
114
        try {
115
            $data = \ExceptionalJSON\decode($json, false, $depthLimit, $options & ~\JSON_OBJECT_AS_ARRAY);
116
117
            $doc = new self();
118
            $doc->rootNode = self::getSafeNodeFactory()->createNodeFromValue($doc, $data);
119
120
            return $doc;
121
        } catch (DecodeErrorException $e) {
122
            throw new ParseFailureException("Decoding JSON string failed: {$e->getMessage()}", $e);
123
        } catch (InvalidNodeValueException $e) {
124
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
125
        } catch (\Throwable $e) {
126
            throw new DocumentTreeCreationFailedException("Unexpected error: {$e->getMessage()}", $e);
127
        }
128
    }
129
130
    /**
131
     * @throws DocumentTreeCreationFailedException
132
     */
133
    public static function createFromData($data): Document
134
    {
135
        try {
136
            $doc = new self();
137
            $doc->rootNode = self::getUnsafeNodeFactory()->createNodeFromValue($doc, $data);
138
139
            return $doc;
140
        } catch (InvalidNodeValueException $e) {
141
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
142
        } catch (\Throwable $e) {
143
            throw new DocumentTreeCreationFailedException("Unexpected error: {$e->getMessage()}", $e);
144
        }
145
    }
146
147
    public function getRootNode(): ?Node
148
    {
149
        return $this->rootNode;
150
    }
151
152
    /**
153
     * @throws InvalidSubjectNodeException
154
     * @throws WriteOperationForbiddenException
155
     * @throws InvalidNodeValueException
156
     * @throws InvalidSubjectNodeException
157
     */
158
    public function import(Node $node): Node
159
    {
160
        if ($node->getOwnerDocument() === $this) {
161
            throw new InvalidSubjectNodeException('Cannot import tne supplied node, already owned by this document');
162
        }
163
164
        if ($node instanceof NullNode) {
165
            return new NullNode($this);
166
        }
167
168
        return $node instanceof VectorNode
169
            ? $this->importVectorNode($node)
170
            : $this->importScalarNode($node);
171
    }
172
173
    /**
174
     * @param Pointer|string $pointer
175
     * @return Node|int|string
176
     * @throws InvalidPointerException
177
     * @throws PointerReferenceNotFoundException
178
     * @throws InvalidSubjectNodeException
179
     */
180
    public function evaluatePointer($pointer, Node $base = null)
181
    {
182
        if (!($pointer instanceof Pointer)) {
183
            $pointer = Pointer::createFromString((string)$pointer);
184
        }
185
186
        if (!$pointer->isRelative()) {
187
            return $this->evaluatePointerPath($pointer, $this->rootNode);
188
        }
189
190
        $target = $this->evaluateRelativePointer($pointer, $base ?? $this->rootNode);
191
192
        return $pointer->isKeyLookup()
193
            ? $target->getKey()
194
            : $target;
195
    }
196
197
    public function jsonSerialize()
198
    {
199
        return $this->rootNode !== null
200
            ? $this->rootNode->jsonSerialize()
201
            : null;
202
    }
203
}
204