Completed
Push — master ( f400d6...2675c1 )
by Chris
02:17
created

Document::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 1
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 1
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 0
nc 1
nop 0
crap 2
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
        } catch (\Exception $e) {
96
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}");
97
        }
98
    }
99
100
    private function __construct() { }
101
102
    /**
103
     * @throws DocumentTreeCreationFailedException
104
     * @throws ParseFailureException
105
     */
106
    public static function parse(string $json, int $depthLimit = 512, int $options = 0): Document
107
    {
108
        static $nodeFactory;
109
110
        try {
111
            $data = \ExceptionalJSON\decode($json, false, $depthLimit, $options & ~\JSON_OBJECT_AS_ARRAY);
112
113
            $doc = new self();
114
            $doc->rootNode = ($nodeFactory ?? $nodeFactory = new SafeNodeFactory)
115
                ->createNodeFromValue($data, $doc);
116
117
            return $doc;
118
        } catch (DecodeErrorException $e) {
119
            throw new ParseFailureException("Decoding JSON string failed: {$e->getMessage()}", $e);
120
        } catch (InvalidNodeValueException $e) {
121
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
122
        } catch (\Exception $e) {
123
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}");
124
        }
125
    }
126
127
    /**
128
     * @throws DocumentTreeCreationFailedException
129
     */
130 12
    public static function createFromValue($value): Document
131
    {
132
        try {
133 12
            $doc = new self();
134 12
            $doc->rootNode = Node::createFromValue($value, $doc);
135
136 12
            return $doc;
137
        } catch (InvalidNodeValueException $e) {
138
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
139
        } catch (\Exception $e) {
140
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}");
141
        }
142
    }
143
144 10
    public function getRootNode(): ?Node
145
    {
146 10
        return $this->rootNode;
147
    }
148
149
    /**
150
     * @throws InvalidSubjectNodeException
151
     * @throws WriteOperationForbiddenException
152
     * @throws InvalidNodeValueException
153
     * @throws InvalidSubjectNodeException
154
     */
155
    public function import(Node $node): Node
156
    {
157
        if ($node->getOwnerDocument() === $this) {
158
            throw new InvalidSubjectNodeException('Cannot import tne supplied node, already owned by this document');
159
        }
160
161
        if ($node instanceof NullNode) {
162
            return new NullNode($this);
163
        }
164
165
        return $node instanceof VectorNode
166
            ? $this->importVectorNode($node)
167
            : $this->importScalarNode($node);
168
    }
169
170
    /**
171
     * @param Pointer|string $pointer
172
     * @return Node|int|string
173
     * @throws InvalidPointerException
174
     * @throws PointerReferenceNotFoundException
175
     * @throws InvalidSubjectNodeException
176
     */
177
    public function evaluatePointer($pointer, Node $base = null)
178
    {
179
        if (!($pointer instanceof Pointer)) {
180
            $pointer = Pointer::createFromString((string)$pointer);
181
        }
182
183
        if (!$pointer->isRelative()) {
184
            return $this->evaluatePointerPath($pointer, $this->rootNode);
185
        }
186
187
        $target = $this->evaluateRelativePointer($pointer, $base ?? $this->rootNode);
188
189
        return $pointer->isKeyLookup()
190
            ? $target->getKey()
191
            : $target;
192
    }
193
194
    public function jsonSerialize()
195
    {
196
        return $this->rootNode !== null
197
            ? $this->rootNode->jsonSerialize()
198
            : null;
199
    }
200
}
201