Completed
Push — master ( 043491...033c1a )
by Nate
02:58
created

JsonElementReader::getPath()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 15
Code Lines 8

Duplication

Lines 15
Ratio 100 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
dl 15
loc 15
ccs 8
cts 8
cp 1
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 8
nc 5
nop 0
crap 6
1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
namespace Tebru\Gson\Internal;
8
9
use ArrayIterator;
10
use Iterator;
11
use Tebru\Gson\Element\JsonArray;
12
use Tebru\Gson\Element\JsonElement;
13
use Tebru\Gson\Element\JsonNull;
14
use Tebru\Gson\Element\JsonObject;
15
use Tebru\Gson\Element\JsonPrimitive;
16
use Tebru\Gson\Exception\UnexpectedJsonTokenException;
17
use Tebru\Gson\JsonReadable;
18
use Tebru\Gson\JsonToken;
19
20
/**
21
 * Class JsonElementReader
22
 *
23
 * @author Nate Brunette <[email protected]>
24
 */
25
final class JsonElementReader implements JsonReadable
26
{
27
    /**
28
     * A stack representing the next element to be consumed
29
     *
30
     * @var array
31
     */
32
    private $stack = [];
33
34
    /**
35
     * An array of types that map to the position in the stack
36
     *
37
     * @var array
38
     */
39
    private $stackTypes = [];
40
41
    /**
42
     * The current size of the stack
43
     *
44
     * @var int
45
     */
46
    private $stackSize = 0;
47
48
    /**
49
     * A cache of the current [@see JsonToken].  This should get nulled out
50
     * whenever a new token should be returned with the subsequent call
51
     * to [@see JsonDecodeReader::peek]
52
     *
53
     * @var
54
     */
55
    private $currentToken;
56
57
    /**
58
     * An array of path names that correspond to the current stack
59
     *
60
     * @var array
61
     */
62
    private $pathNames = [];
63
64
    /**
65
     * An array of path indicies that correspond to the current stack. This array could contain invalid
66
     * values at indexes outside the current stack. It could also contain incorrect values at indexes
67
     * where a path name is used. Data should only be fetched by referencing the current position in the stack.
68
     *
69
     * @var array
70
     */
71
    private $pathIndices = [];
72
73
    /**
74
     * Constructor
75
     *
76
     * @param JsonElement $jsonElement
77
     */
78 69
    public function __construct(JsonElement $jsonElement)
79
    {
80 69
        $this->push($jsonElement);
81 69
    }
82
83
    /**
84
     * Consumes the next token and asserts it's the beginning of a new array
85
     *
86
     * @return void
87
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_ARRAY
88
     */
89 15 View Code Duplication
    public function beginArray(): void
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
90
    {
91 15
        $this->expect(JsonToken::BEGIN_ARRAY);
92
93
        /** @var JsonArray $jsonArray */
94 14
        $jsonArray = $this->pop();
95 14
        $this->push($jsonArray->getIterator(), ArrayIterator::class);
96 14
        $this->pathIndices[$this->stackSize - 1] = 0;
97 14
    }
98
99
    /**
100
     * Consumes the next token and asserts it's the end of an array
101
     *
102
     * @return void
103
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_ARRAY
104
     */
105 5
    public function endArray(): void
106
    {
107 5
        $this->expect(JsonToken::END_ARRAY);
108
109 4
        $this->pop();
110 4
        $this->incrementPathIndex();
111 4
    }
112
113
    /**
114
     * Consumes the next token and asserts it's the beginning of a new object
115
     *
116
     * @return void
117
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BEGIN_OBJECT
118
     */
119 17
    public function beginObject(): void
120
    {
121 17
        $this->expect(JsonToken::BEGIN_OBJECT);
122
123
        /** @var JsonObject $jsonObject */
124 16
        $jsonObject = $this->pop();
125
126 16
        $this->push(new JsonObjectIterator($jsonObject), JsonObjectIterator::class);
127 16
    }
128
129
    /**
130
     * Consumes the next token and asserts it's the end of an object
131
     *
132
     * @return void
133
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not END_OBJECT
134
     */
135 4
    public function endObject(): void
136
    {
137 4
        $this->expect(JsonToken::END_OBJECT);
138
139 3
        $this->pop();
140 3
        $this->incrementPathIndex();
141 3
    }
142
143
    /**
144
     * Returns true if the array or object has another element
145
     *
146
     * If the current scope is not an array or object, this returns false
147
     *
148
     * @return bool
149
     */
150 4
    public function hasNext(): bool
151
    {
152 4
        $peek = $this->peek();
153
154 4
        return $peek !== JsonToken::END_OBJECT && $peek !== JsonToken::END_ARRAY;
155
    }
156
157
    /**
158
     * Consumes the value of the next token, asserts it's a boolean and returns it
159
     *
160
     * @return bool
161
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not BOOLEAN
162
     */
163 6
    public function nextBoolean(): bool
164
    {
165 6
        $this->expect(JsonToken::BOOLEAN);
166
167
        /** @var JsonPrimitive $primitive */
168 5
        $primitive = $this->pop();
169
170 5
        $this->incrementPathIndex();
171
172 5
        return $primitive->asBoolean();
173
    }
174
175
    /**
176
     * Consumes the value of the next token, asserts it's a double and returns it
177
     *
178
     * @return double
179
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER
180
     */
181 3
    public function nextDouble(): float
182
    {
183 3
        $this->expect(JsonToken::NUMBER);
184
185
        /** @var JsonPrimitive $primitive */
186 2
        $primitive = $this->pop();
187
188 2
        $this->incrementPathIndex();
189
190 2
        return $primitive->asFloat();
191
    }
192
193
    /**
194
     * Consumes the value of the next token, asserts it's an int and returns it
195
     *
196
     * @return int
197
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NUMBER
198
     */
199 6
    public function nextInteger(): int
200
    {
201 6
        $this->expect(JsonToken::NUMBER);
202
203
        /** @var JsonPrimitive $primitive */
204 5
        $primitive = $this->pop();
205
206 5
        $this->incrementPathIndex();
207
208 5
        return $primitive->asInteger();
209
    }
210
211
    /**
212
     * Consumes the value of the next token, asserts it's a string and returns it
213
     *
214
     * @return string
215
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or STRING
216
     */
217 14 View Code Duplication
    public function nextString(): string
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
    {
219 14
        $peek = $this->peek();
220 14
        if ($peek === JsonToken::NAME) {
221 1
            return $this->nextName();
222
        }
223
224 13
        $this->expect(JsonToken::STRING);
225
226
        /** @var JsonPrimitive $primitive */
227 12
        $primitive = $this->pop();
228
229 12
        $this->incrementPathIndex();
230
231 12
        return $primitive->asString();
232
    }
233
234
    /**
235
     * Consumes the value of the next token and asserts it's null
236
     *
237
     * @return null
238
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME or NULL
239
     */
240 2
    public function nextNull()
241
    {
242 2
        $this->expect(JsonToken::NULL);
243
244 1
        $this->pop();
245
246 1
        $this->incrementPathIndex();
247
248 1
        return null;
249
    }
250
251
    /**
252
     * Consumes the next name and returns it
253
     *
254
     * @return string
255
     * @throws \Tebru\Gson\Exception\UnexpectedJsonTokenException If the next token is not NAME
256
     */
257 9 View Code Duplication
    public function nextName(): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
258
    {
259 9
        $this->expect(JsonToken::NAME);
260
261
        /** @var JsonObjectIterator $iterator */
262 9
        $iterator = $this->stack[$this->stackSize - 1];
263 9
        $key = $iterator->key();
264 9
        $value = $iterator->current();
265 9
        $iterator->next();
266
267 9
        $this->pathNames[$this->stackSize - 1] = $key;
268
269 9
        $this->push($value);
270
271 9
        return $key;
272
    }
273
274
    /**
275
     * Returns an enum representing the type of the next token without consuming it
276
     *
277
     * @return string
278
     */
279 69
    public function peek(): string
280
    {
281 69
        if (null !== $this->currentToken) {
282 23
            return $this->currentToken;
283
        }
284
285 69
        if (0 === $this->stackSize) {
286 1
            $this->currentToken = JsonToken::END_DOCUMENT;
287
288 1
            return $this->currentToken;
289
        }
290
291 69
        $token = null;
292 69
        $element = $this->stack[$this->stackSize - 1];
293
294 69
        switch ($this->stackTypes[$this->stackSize - 1]) {
295 69
            case JsonArray::class:
296 16
                $token = JsonToken::BEGIN_ARRAY;
297 16
                break;
298 66
            case JsonNull::class:
299 2
                $token = JsonToken::NULL;
300 2
                break;
301 64
            case JsonObject::class:
302 20
                $token = JsonToken::BEGIN_OBJECT;
303 20
                break;
304 61
            case JsonPrimitive::class:
305 45
                if ($element->isString()) {
306 18
                    $token = JsonToken::STRING;
307 28
                } elseif ($element->isBoolean()) {
308 8
                    $token = JsonToken::BOOLEAN;
309 21
                } elseif ($element->isNumber()) {
310 21
                    $token = JsonToken::NUMBER;
311
                }
312
313 45
                break;
314 27
            case JsonObjectIterator::class:
315 15
                $token = $element->valid() ? JsonToken::NAME : JsonToken::END_OBJECT;
316
317 15
                break;
318 13 View Code Duplication
            case ArrayIterator::class:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319 13
                if ($element->valid()) {
320 9
                    $this->push($element->current());
321 9
                    $element->next();
322
323 9
                    $token = $this->peek();
324
                } else {
325 7
                    $token = JsonToken::END_ARRAY;
326
                }
327
328 13
                break;
329
        }
330
331 69
        $this->currentToken = $token;
332
333 69
        return $this->currentToken;
334
    }
335
336
    /**
337
     * Skip the next value.  If the next value is an object or array, all children will
338
     * also be skipped.
339
     *
340
     * @return void
341
     */
342 1
    public function skipValue(): void
343
    {
344 1
        $this->pop();
345 1
    }
346
347
    /**
348
     * Get the current read path in json xpath format
349
     *
350
     * @return string
351
     */
352 10 View Code Duplication
    public function getPath(): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
353
    {
354 10
        $result = '$';
355 10
        foreach ($this->stack as $index => $item) {
356 10
            if ($item instanceof ArrayIterator && isset($this->pathIndices[$index])) {
357 1
                $result .= '['.$this->pathIndices[$index].']';
358
            }
359
360 10
            if ($item instanceof JsonObjectIterator && isset($this->pathNames[$index])) {
361 10
                $result .= '.'.$this->pathNames[$index];
362
            }
363
        }
364
365 10
        return $result;
366
    }
367
368
    /**
369
     * Push an element onto the stack
370
     *
371
     * @param JsonElement|Iterator $element
372
     * @param string $type
0 ignored issues
show
Documentation introduced by
Should the type for parameter $type not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
373
     */
374 69 View Code Duplication
    private function push($element, $type = null): void
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
375
    {
376 69
        if (null === $type) {
377 69
            $type = get_class($element);
1 ignored issue
show
Coding Style introduced by
Consider using a different name than the parameter $type. This often makes code more readable.
Loading history...
378
        }
379
380 69
        $this->stack[$this->stackSize] = $element;
381 69
        $this->stackTypes[$this->stackSize] = $type;
382 69
        $this->stackSize++;
383 69
        $this->currentToken = null;
384 69
    }
385
386
    /**
387
     * Pop the last element off the stack and return it
388
     *
389
     * @return JsonElement|Iterator
390
     */
391 45 View Code Duplication
    private function pop()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
392
    {
393 45
        $this->stackSize--;
394 45
        array_pop($this->stackTypes);
395 45
        $this->currentToken = null;
396
397 45
        return array_pop($this->stack);
398
    }
399
400
    /**
401
     * Check that the next token equals the expectation
402
     *
403
     * @param string $expectedToken
404
     * @throws UnexpectedJsonTokenException
405
     */
406 52 View Code Duplication
    private function expect(string $expectedToken)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
407
    {
408 52
        if ($this->peek() === $expectedToken) {
409 45
            return;
410
        }
411
412
        // increment the path index because exceptions are thrown before this value is increased. We
413
        // want to display the current index that has a problem.
414 10
        $this->incrementPathIndex();
415
416 10
        throw new UnexpectedJsonTokenException(
417 10
            sprintf('Expected "%s", but found "%s" at "%s"', $expectedToken, $this->peek(), $this->getPath())
418
        );
419
    }
420
421
    /**
422
     * Increment the path index. This should be called any time a new value is parsed.
423
     */
424 36 View Code Duplication
    private function incrementPathIndex(): void
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
425
    {
426 36
        $index = $this->stackSize - 1;
427 36
        if ($index >= 0) {
428 17
            if (!isset($this->pathIndices[$index])) {
429 14
                $this->pathIndices[$index] = 0;
430
            }
431 17
            $this->pathIndices[$index]++;
432
        }
433 36
    }
434
}
435