Completed
Push — master ( 9afc27...035c9e )
by Chris
04:26
created

PointerEvaluator   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 159
Duplicated Lines 0 %

Test Coverage

Coverage 85%

Importance

Changes 0
Metric Value
wmc 27
dl 0
loc 159
ccs 51
cts 60
cp 0.85
rs 10
c 0
b 0
f 0

8 Methods

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