Completed
Push — master ( 5d9a5d...e6d18b )
by Chris
02:53
created

Document::createFromValue()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.3332

Importance

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