Passed
Pull Request — master (#5)
by Nate
03:04
created

JsonReader::push()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1
ccs 0
cts 0
cp 0
c 0
b 0
f 0
nc 1
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 Iterator;
10
use Tebru\Gson\Element\JsonElement;
11
use Tebru\Gson\Exception\JsonSyntaxException;
12
use Tebru\Gson\JsonReadable;
13
use Tebru\Gson\JsonToken;
14
15
/**
16
 * Class JsonReader
17
 *
18
 * @author Nate Brunette <[email protected]>
19
 */
20
abstract class JsonReader implements JsonReadable
21
{
22
    /**
23
     * A stack representing the next element to be consumed
24
     *
25
     * @var array
26
     */
27
    protected $stack = [];
28
29
    /**
30
     * An array of types that map to the position in the stack
31
     *
32
     * @var array
33
     */
34
    protected $stackTypes = [];
35
36
    /**
37
     * The current size of the stack
38
     *
39
     * @var int
40
     */
41
    protected $stackSize = 0;
42
43
    /**
44
     * An array of path names that correspond to the current stack
45
     *
46
     * @var array
47
     */
48
    protected $pathNames = [];
49
50
    /**
51
     * An array of path indicies that correspond to the current stack. This array could contain invalid
52
     * values at indexes outside the current stack. It could also contain incorrect values at indexes
53
     * where a path name is used. Data should only be fetched by referencing the current position in the stack.
54
     *
55
     * @var array
56
     */
57
    protected $pathIndices = [];
58
59
    /**
60
     * A cache of the current [@see JsonToken].  This should get nulled out
61
     * whenever a new token should be returned with the subsequent call
62
     * to [@see JsonDecodeReader::peek]
63
     *
64
     * @var int|null
65
     */
66
    protected $currentToken;
67
68
    /**
69
     * The original payload
70
     *
71
     * @var mixed
72
     */
73
    protected $payload;
74
75
    /**
76
     * Returns an enum representing the type of the next token without consuming it
77
     *
78
     * @return string
79
     */
80
    abstract public function peek(): string;
81
82
    /**
83
     * Get the current read path in json xpath format
84
     *
85
     * @return string
86
     */
87
    abstract public function getPath(): string;
88
89
    /**
90
     * Push an element onto the stack
91
     *
92
     * @param JsonElement|Iterator $element
93
     * @param string|null $type
94
     * @return void
95
     */
96
    abstract protected function push($element, $type = null): void;
97
98
    /**
99
     * Consumes the next token and asserts it's the end of an array
100
     *
101
     * @return void
102
     * @throws \Tebru\Gson\Exception\JsonSyntaxException If the next token is not END_ARRAY
103
     */
104 13
    public function endArray(): void
105
    {
106 13
        $this->expect(JsonToken::END_ARRAY);
107
108 11
        $this->pop();
109 11
        $this->incrementPathIndex();
110 11
    }
111
112
    /**
113
     * Consumes the next token and asserts it's the end of an object
114
     *
115
     * @return void
116
     * @throws \Tebru\Gson\Exception\JsonSyntaxException If the next token is not END_OBJECT
117
     */
118 12
    public function endObject(): void
119
    {
120 12
        $this->expect(JsonToken::END_OBJECT);
121
122 10
        $this->pop();
123 10
        $this->incrementPathIndex();
124 10
    }
125
126
    /**
127
     * Returns true if the array or object has another element
128
     *
129
     * If the current scope is not an array or object, this returns false
130
     *
131
     * @return bool
132
     */
133 11
    public function hasNext(): bool
134
    {
135 11
        $peek = $this->peek();
136
137 11
        return $peek !== JsonToken::END_OBJECT && $peek !== JsonToken::END_ARRAY;
138
    }
139
140
    /**
141
     * Consumes the next name and returns it
142
     *
143
     * @return string
144
     * @throws \Tebru\Gson\Exception\JsonSyntaxException If the next token is not NAME
145
     */
146 26
    public function nextName(): string
147
    {
148 26
        $this->expect(JsonToken::NAME);
149
150
        /** @var Iterator $iterator */
151 26
        $iterator = $this->stack[$this->stackSize - 1];
152 26
        $key = $iterator->key();
153 26
        $value = $iterator->current();
154 26
        $iterator->next();
155
156 26
        $this->pathNames[$this->stackSize - 1] = $key;
157
158 26
        $this->push($value);
159
160 26
        return (string)$key;
161
    }
162
163
    /**
164
     * Consumes the value of the next token and asserts it's null
165
     *
166
     * @return null
167
     * @throws \Tebru\Gson\Exception\JsonSyntaxException If the next token is not NAME or NULL
168
     */
169 4
    public function nextNull()
170
    {
171 4
        $this->expect(JsonToken::NULL);
172
173 2
        $this->pop();
174
175 2
        $this->incrementPathIndex();
176
177 2
        return null;
178
    }
179
180
    /**
181
     * Skip the next value.  If the next value is an object or array, all children will
182
     * also be skipped.
183
     *
184
     * @return void
185
     */
186 2
    public function skipValue(): void
187
    {
188 2
        $this->pop();
189 2
    }
190
191
    /**
192
     * Returns the original json after json_decode
193
     *
194
     * @return mixed
195
     */
196 2
    public function getPayload()
197
    {
198 2
        return $this->payload;
199
    }
200
201
    /**
202
     * Pop the last element off the stack and return it
203
     *
204
     * @return JsonElement|Iterator|mixed
205
     */
206 106
    protected function pop()
207
    {
208 106
        $this->stackSize--;
209 106
        array_pop($this->stackTypes);
210 106
        $this->currentToken = null;
211
212 106
        return array_pop($this->stack);
213
    }
214
215
    /**
216
     * Check that the next token equals the expectation
217
     *
218
     * @param string $expectedToken
219
     * @throws \Tebru\Gson\Exception\JsonSyntaxException If the next token is not the expectation
220
     */
221 117
    protected function expect(string $expectedToken)
222
    {
223 117
        if ($this->peek() === $expectedToken) {
224 106
            return;
225
        }
226
227
        // increment the path index because exceptions are thrown before this value is increased. We
228
        // want to display the current index that has a problem.
229 20
        $this->incrementPathIndex();
230
231 20
        throw new JsonSyntaxException(
232 20
            sprintf('Expected "%s", but found "%s" at "%s"', $expectedToken, $this->peek(), $this->getPath())
233
        );
234
    }
235
236
    /**
237
     * Increment the path index. This should be called any time a new value is parsed.
238
     */
239 82
    protected function incrementPathIndex(): void
240
    {
241 82
        $index = $this->stackSize - 1;
242 82
        if ($index >= 0) {
243 44
            if (!isset($this->pathIndices[$index])) {
244 33
                $this->pathIndices[$index] = 0;
245
            }
246 44
            $this->pathIndices[$index]++;
247
        }
248 82
    }
249
}
250