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