Completed
Push — master ( 25e428...866afb )
by Chris
03:20
created

PointerEvaluator::getArrayIndexFromPathComponent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 4
crap 2
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Jom;
4
5
use DaveRandom\Jom\Exceptions\InvalidKeyException;
6
use DaveRandom\Jom\Exceptions\InvalidPointerException;
7
use DaveRandom\Jom\Exceptions\InvalidReferenceNodeException;
8
use DaveRandom\Jom\Exceptions\PointerReferenceNotFoundException;
9
10
final class PointerEvaluator
11
{
12
    private $root;
13
14
    /**
15
     * @throws PointerReferenceNotFoundException
16
     */
17 4
    private function getValidArrayIndex(string $index, Pointer $pointer, int $level): int
18
    {
19 4
        if (\preg_match('/^(?:0|[1-9][0-9]*)$/', $index)) {
20 4
            return (int)$index;
21
        }
22
23
        throw new PointerReferenceNotFoundException(
24
            "Array member must be referenced by integer index without leading zero", $pointer, $level
25
        );
26
    }
27
28
    /**
29
     * @throws PointerReferenceNotFoundException
30
     */
31 4
    private function getArrayIndexFromPathComponent(Pointer $pointer, ArrayNode $node, string $index, int $level)
32
    {
33
        try {
34 4
            return $node->item($this->getValidArrayIndex($index, $pointer, $level));
35 1
        } catch (InvalidKeyException $e) {
36 1
            throw new PointerReferenceNotFoundException(
37 1
                "The referenced index does not exist", $pointer, $level, $e
38
            );
39
        }
40
    }
41
42
    /**
43
     * @throws PointerReferenceNotFoundException
44
     */
45 14
    private function getObjectPropertyFromPathComponent(Pointer $pointer, ObjectNode $node, string $name, int $level)
46
    {
47
        try {
48 14
            return $node->getProperty($name);
49 2
        } catch (InvalidKeyException $e) {
50 2
            throw new PointerReferenceNotFoundException(
51 2
                'The referenced property does not exist', $pointer, $level, $e
52
            );
53
        }
54
    }
55
56
    /**
57
     * @throws PointerReferenceNotFoundException
58
     */
59 37
    private function evaluatePointerPath(Pointer $pointer, Node $current): Node
60
    {
61 37
        static $nodeTypeNames = [
62
            BooleanNode::class => 'boolean',
63
            NumberNode::class => 'number',
64
            StringNode::class => 'string',
65
            NullNode::class => 'null',
66
        ];
67
68 37
        foreach ($pointer->getPath() as $level => $component) {
69 16
            if ($current instanceof ObjectNode) {
70 14
                $current = $this->getObjectPropertyFromPathComponent($pointer, $current, $component, $level);
71 13
                continue;
72
            }
73
74 4
            if ($current instanceof ArrayNode) {
75 4
                $current = $this->getArrayIndexFromPathComponent($pointer, $current, $component, $level);
76 3
                continue;
77
            }
78
79
            $type = $nodeTypeNames[\get_class($current)] ?? 'unknown type';
80
81
            throw new PointerReferenceNotFoundException(
82
                "Expecting object or array, got {$type}", $pointer, $level
83
            );
84
        }
85
86 34
        return $current;
87
    }
88
89
    /**
90
     * @throws InvalidReferenceNodeException
91
     */
92 36
    private function validateRelativePointerContextNode(Pointer $pointer, Node $context): void
93
    {
94 36
        if ($context->getOwnerDocument() !== $this->root->getOwnerDocument()) {
95
            throw new InvalidReferenceNodeException(
96
                'Context node for relative pointer evaluation does not belong to the same document'
97
                . ' as the evaluator root node'
98
            );
99
        }
100
101 36
        if ($pointer->getRelativeLevels() > 0 && !$this->root->containsChild($context)) {
102
            throw new InvalidReferenceNodeException(
103
                'Context node for relative pointer evaluation is not a child of the root node'
104
            );
105
        }
106
    }
107
108
    /**
109
     * @throws PointerReferenceNotFoundException
110
     * @throws InvalidReferenceNodeException
111
     */
112 42
    private function evaluateRelativePointer(Pointer $pointer, Node $current): Node
113
    {
114 42
        if ($current !== $this->root) {
115 36
            $this->validateRelativePointerContextNode($pointer, $current);
116
        }
117
118 42
        for ($i = 0, $levels = $pointer->getRelativeLevels(); $i < $levels; $i++) {
119 30
            if (($current ?? $this->root) === $this->root) {
120 12
                throw new PointerReferenceNotFoundException(
121 12
                    "Relative pointer prefix overflows context node nesting level {$i}", $pointer
122
                );
123
            }
124
125 27
            $current = $current->getParent();
126
        }
127
128 30
        return $this->evaluatePointerPath($pointer, $current);
129
    }
130
131
    /**
132
     * @param Node|Document $root
133
     * @throws InvalidReferenceNodeException
134
     */
135 52
    public function __construct($root)
136
    {
137 52
        if ($root instanceof Document) {
138 1
            $root = $root->getRootNode();
139
        }
140
141 52
        if (!($root instanceof Node)) {
142 1
            throw new InvalidReferenceNodeException(
143 1
                'Pointer evaluator root node must be instance of ' . Node::class . ' or ' . Document::class
144
            );
145
        }
146
147 51
        $this->root = $root;
148
    }
149
150 2
    public function getRootNode(): Node
151
    {
152 2
        return $this->root;
153
    }
154
155
    /**
156
     * @param Pointer|string $pointer
157
     * @return Node|int|string
158
     * @throws InvalidPointerException
159
     * @throws InvalidReferenceNodeException
160
     * @throws PointerReferenceNotFoundException
161
     */
162 49
    public function evaluatePointer($pointer, ?Node $context = null)
163
    {
164 49
        if (!($pointer instanceof Pointer)) {
165 49
            $pointer = Pointer::createFromString((string)$pointer);
166
        }
167
168 49
        if (!$pointer->isRelative()) {
169 7
            return $this->evaluatePointerPath($pointer, $this->root);
170
        }
171
172 42
        $target = $this->evaluateRelativePointer($pointer, $context ?? $this->root);
173
174 30
        return $pointer->isKeyLookup()
175 10
            ? $target->getKey()
176 30
            : $target;
177
    }
178
}
179