Completed
Push — master ( a6b4b4...48311f )
by Chris
02:50
created

PointerEvaluator::evaluatePointerPath()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.3244

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 2
dl 0
loc 21
ccs 8
cts 11
cp 0.7272
crap 4.3244
rs 9.0534
c 0
b 0
f 0
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 const NODE_TYPE_NAMES  = [
13
        BooleanNode::class => 'boolean',
14
        NumberNode::class => 'number',
15
        StringNode::class => 'string',
16
        NullNode::class => 'null',
17 4
    ];
18
19 4
    private $root;
20 4
21
    /**
22
     * @throws PointerReferenceNotFoundException
23
     */
24
    private function getValidArrayIndex(string $index, Pointer $pointer, int $level): int
25
    {
26
        if (\preg_match('/^(?:0|[1-9][0-9]*)$/', $index)) {
27
            return (int)$index;
28
        }
29
30
        throw new PointerReferenceNotFoundException(
31 4
            "Array member must be referenced by integer index without leading zero", $pointer, $level
32
        );
33
    }
34 4
35 1
    /**
36 1
     * @throws PointerReferenceNotFoundException
37 1
     */
38
    private function getArrayIndexFromPathComponent(Pointer $pointer, ArrayNode $node, string $index, int $level)
39
    {
40
        try {
41
            return $node->item($this->getValidArrayIndex($index, $pointer, $level));
42
        } catch (InvalidKeyException $e) {
43
            throw new PointerReferenceNotFoundException(
44
                "The referenced index does not exist", $pointer, $level, $e
45 14
            );
46
        }
47
    }
48 14
49 2
    /**
50 2
     * @throws PointerReferenceNotFoundException
51 2
     */
52
    private function getObjectPropertyFromPathComponent(Pointer $pointer, ObjectNode $node, string $name, int $level)
53
    {
54
        try {
55
            return $node->getProperty($name);
56
        } catch (InvalidKeyException $e) {
57
            throw new PointerReferenceNotFoundException(
58
                'The referenced property does not exist', $pointer, $level, $e
59 37
            );
60
        }
61 37
    }
62
63
    /**
64
     * @throws PointerReferenceNotFoundException
65
     */
66
    private function evaluatePointerPath(Pointer $pointer, Node $current): Node
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
            $typeName = self::NODE_TYPE_NAMES[\get_class($current)] ?? 'unknown type';
80
81
            throw new PointerReferenceNotFoundException(
82
                "Expecting object or array, got {$typeName}", $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