Passed
Push — master ( e7798d...4b26b6 )
by Mikhail
02:39
created

Parser::fromString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace JsonDecodeStream;
5
6
use Generator;
7
use JsonDecodeStream\Collector\Collector;
8
use JsonDecodeStream\Collector\CollectorInterface;
9
use JsonDecodeStream\Exception\ParserException;
10
use JsonDecodeStream\Internal\SourceBuffer;
11
use JsonDecodeStream\Internal\Stack;
12
use JsonDecodeStream\Internal\StackFrame;
13
use JsonDecodeStream\Source\FileSource;
14
use JsonDecodeStream\Source\Psr7Source;
15
use JsonDecodeStream\Source\SourceInterface;
16
use JsonDecodeStream\Source\StreamSource;
17
use JsonDecodeStream\Source\StringSource;
18
use Psr\Http\Message\StreamInterface;
19
20
class Parser
21
{
22
    /** @var SourceBuffer */
23
    protected $buffer;
24
25
    /** @var Stack */
26
    protected $stack;
27
28 44
    public function __construct(SourceInterface $source)
29
    {
30 44
        $this->buffer = new SourceBuffer($source);
31 44
    }
32
33 17
    public static function fromString(string $string)
34
    {
35 17
        return new static(new StringSource($string));
36
    }
37
38 27
    public static function fromFile(string $path)
39
    {
40 27
        return new static(new FileSource($path));
41
    }
42
43
    public static function fromStream($stream)
44
    {
45
        return new static(new StreamSource($stream));
46
    }
47
48
    public static function fromPsr7(StreamInterface $stream)
49
    {
50
        return new static(new Psr7Source($stream));
51
    }
52
53
    /**
54
     * @param string|string[]|Collector|Collector[] $selectors
55
     * single or coma-separated selector string
56
     * or custom CollectorInterface implementation
57
     * or array of any of both
58
     * @param bool                                  $objectsAsAssoc
59
     * @return iterable|Generator
60
     * @throws Exception\CollectorException
61
     * @throws Exception\SelectorException
62
     * @throws Exception\TokenizerException
63
     * @throws ParserException
64
     */
65 7
    public function items($selectors, bool $objectsAsAssoc = false)
66
    {
67 7
        if (is_string($selectors)) {
68 5
            $selectorsArray = explode(',', $selectors);
69 2
        } else if (is_array($selectors)) {
70
            $selectorsArray = $selectors;
71 2
        } else if ($selectors instanceof CollectorInterface) {
0 ignored issues
show
introduced by
$selectors is always a sub-type of JsonDecodeStream\Collector\CollectorInterface.
Loading history...
72 2
            $selectorsArray = [ $selectors ];
73
        } else {
74
            throw new ParserException('Unexpected selectors are provided', ParserException::CODE_INVALID_ARGUMENT);
75
        }
76 7
        if (empty($selectorsArray)) {
77
            throw new ParserException('No selectors are provided', ParserException::CODE_INVALID_ARGUMENT);
78
        }
79 7
        $collectors = [];
80 7
        foreach ($selectorsArray as $selector) {
81 7
            if (is_string($selector)) {
82 5
                $collectors[] = new Collector($selector, $objectsAsAssoc);
83 2
            } elseif ($selector instanceof CollectorInterface) {
84 2
                $collectors[] = $selector;
85
            } else {
86
                throw new ParserException(
87
                    'Invalid collector: '
88
                    . is_object($selector) ? get_class($selector) : gettype($selector),
89
                    ParserException::CODE_INVALID_ARGUMENT
90
                );
91
            }
92
        }
93
94 7
        foreach ($this->events() as $event) {
95 7
            foreach ($collectors as $collector) {
96 7
                $yielded = $collector->processEvent($event);
97 7
                if (is_array($yielded)) {
98 6
                    if (count($yielded) != 2) {
99
                        throw ParserException::unexpectedCollectorReturn($yielded, $event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of JsonDecodeStream\Excepti...eturn($yielded, $event) targeting JsonDecodeStream\Excepti...pectedCollectorReturn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
100
                    }
101 6
                    [ $key, $value ] = $yielded;
102 6
                    yield $key => $value;
103 7
                } else if ($yielded instanceof Generator) {
104 1
                    foreach ($yielded as $yieldedSingle) {
105 1
                        if (!is_array($yieldedSingle) || count($yieldedSingle) != 2) {
106
                            throw ParserException::unexpectedCollectorReturn($yielded, $event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of JsonDecodeStream\Excepti...eturn($yielded, $event) targeting JsonDecodeStream\Excepti...pectedCollectorReturn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
107
                        }
108 1
                        [ $key, $value ] = $yieldedSingle;
109 1
                        yield $key => $value;
110
                    }
111 6
                } else if ($yielded === null) {
112 6
                    continue;
113
                } else {
114
                    throw ParserException::unexpectedCollectorReturn($yielded, $event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of JsonDecodeStream\Excepti...eturn($yielded, $event) targeting JsonDecodeStream\Excepti...pectedCollectorReturn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
115
                }
116
            }
117
        }
118 7
    }
119
120
    /**
121
     * @return Generator|Event[]
122
     * @psalm-return \Generator<Event>
123
     * @throws ParserException
124
     * @noinspection PhpStatementHasEmptyBodyInspection
125
     */
126 44
    public function events(): Generator
127
    {
128 44
        $stack = new Stack();
129 44
        $tokens = $this->tokens();
130
131
        // shortcut to event factory
132 44
        $createEvent = function (string $eventId, $value = null) use ($stack, &$token): Event {
133 44
            return $this->createEvent($eventId, $value, $stack, $token->getLineNumber(), $token->getCharNumber());
134 44
        };
135
136 44
        foreach ($tokens as $token) {
137 44
            if ($token->getId() == Token::WHITESPACE) {
138
                // ignore whitespaces
139 42
                continue;
140
            }
141
142 44
            if ($stack->isEmpty()) {
143 44
                switch ($token->getId()) {
144
                    case Token::OBJECT_START:
145 29
                        yield $createEvent(Event::DOCUMENT_START);
146 29
                        yield $createEvent(Event::OBJECT_START);
147
148 29
                        $stack->push(StackFrame::object());
149 29
                        break;
150
151
                    case Token::ARRAY_START:
152 15
                        yield $createEvent(Event::DOCUMENT_START);
153 15
                        yield $createEvent(Event::ARRAY_START);
154
155 15
                        $stack->push(StackFrame::array());
156 15
                        break;
157
158
                    default:
159
                        throw ParserException::unexpectedToken($token);
160
                }
161 44
                continue;
162
            }
163 44
            if ($stack->current()->isAwaitsComa()) {
164 42
                if ($token->getId() == Token::COMA) {
165 42
                    $stack->current()->setAwaitsComa(false);
166 42
                    if ($stack->current()->isObject()) {
167 29
                        $stack->current()->setLastKey(null);
168 29
                        $stack->current()->setAwaitsKey(true);
169
                    }
170 42
                    continue;
171 42
                } elseif ($stack->current()->isObject() && $token->getId() == Token::OBJECT_END) {
172
                    // pass
173 36
                } elseif ($stack->current()->isArray() && $token->getId() == Token::ARRAY_END) {
174
                    // pass
175
                } else {
176
                    throw ParserException::expectedButGot('","', $token);
177
                }
178
            }
179 44
            if ($stack->current()->isAwaitsKeyDelimiter()) {
180 34
                if ($token->getId() == Token::KEY_DELIMITER) {
181 34
                    $stack->current()->setAwaitsKeyDelimiter(false);
182 34
                    continue;
183
                } else {
184
                    throw ParserException::expectedButGot('":"', $token);
185
                }
186
            }
187 44
            if ($stack->current()->isAwaitsKey()) {
188 35
                if ($token->getId() != Token::STRING && $token->getId() != Token::OBJECT_END) {
189
                    throw ParserException::expectedButGot('object key', $token);
190
                }
191
            }
192
193 44
            if ($stack->current()->isArray()) {
194 37
                switch ($token->getId()) {
195
                    case Token::STRING:
196
                    case Token::NUMBER:
197
                    case Token::NULL:
198
                    case Token::TRUE:
199
                    case Token::FALSE:
200 35
                        $stack->current()->setAwaitsComa(true);
201 35
                        $stack->current()->setLastKey(
202 35
                            $stack->current()->getElementCount()
203
                        );
204 35
                        $stack->current()->incrementElementCount();
205 35
                        yield $createEvent(Event::VALUE, $token->getValue());
206 35
                        break;
207
208
                    case Token::ARRAY_START:
209 7
                        yield $createEvent(Event::ARRAY_START);
210 7
                        $stack->current()->setAwaitsComa(true);
211 7
                        $stack->current()->setLastKey(
212 7
                            $stack->current()->getElementCount()
213
                        );
214 7
                        $stack->current()->incrementElementCount();
215 7
                        $stack->push(StackFrame::array());
216 7
                        break;
217
218
                    case Token::ARRAY_END:
219 37
                        $stack->pop();
220 37
                        yield $createEvent(Event::ARRAY_END);
221 37
                        if ($stack->isEmpty()) {
222 15
                            yield $createEvent(Event::DOCUMENT_END);
223
                        }
224 37
                        break;
225
226
                    case Token::OBJECT_START:
227 27
                        $stack->current()->setLastKey(
228 27
                            $stack->current()->getElementCount()
229
                        );
230 27
                        yield $createEvent(Event::OBJECT_START);
231 27
                        $stack->current()->setAwaitsComa(true);
232 27
                        $stack->current()->incrementElementCount();
233 27
                        $stack->push(StackFrame::object());
234 27
                        break;
235
236
                    case Token::OBJECT_END:
237
                        $stack->pop();
238
                        yield $createEvent(Event::OBJECT_END);
239
                        break;
240
241
                    default:
242
                        throw ParserException::unexpectedToken($token);
243
                }
244
245 37
                continue;
246
            }
247
248 35
            if ($stack->current()->isObject()) {
249 35
                switch ($token->getId()) {
250
                    case Token::STRING:
251 34
                        if ($stack->current()->isAwaitsKey()) {
252 34
                            yield $createEvent(Event::KEY, $token->getValue());
253 34
                            $stack->current()->setLastKey($token->getValue());
254 34
                            $stack->current()->setAwaitsKeyDelimiter(true);
255 34
                            $stack->current()->setAwaitsKey(false);
256
                        } else {
257 25
                            yield $createEvent(Event::VALUE, $token->getValue());
258 25
                            $stack->current()->setAwaitsComa(true);
259 25
                            $stack->current()->incrementElementCount();
260
                        }
261 34
                        break;
262
263
                    case Token::NUMBER:
264
                    case Token::NULL:
265
                    case Token::TRUE:
266
                    case Token::FALSE:
267 28
                        yield $createEvent(Event::VALUE, $token->getValue());
268 28
                        $stack->current()->setAwaitsComa(true);
269 28
                        $stack->current()->incrementElementCount();
270 28
                        break;
271
272
                    case Token::ARRAY_START:
273 28
                        yield $createEvent(Event::ARRAY_START);
274 28
                        $stack->current()->setAwaitsComa(true);
275 28
                        $stack->current()->incrementElementCount();
276 28
                        $stack->push(StackFrame::array());
277 28
                        break;
278
279
                    case Token::ARRAY_END:
280
                        yield $createEvent(Event::ARRAY_END);
281
                        $stack->pop();
282
                        break;
283
284
                    case Token::OBJECT_START:
285 27
                        yield $createEvent(Event::OBJECT_START);
286 27
                        $stack->current()->setAwaitsComa(true);
287 27
                        $stack->current()->incrementElementCount();
288 27
                        $stack->push(StackFrame::object());
289 27
                        break;
290
291
                    case Token::OBJECT_END:
292 35
                        $stack->pop();
293 35
                        yield $createEvent(Event::OBJECT_END);
294 35
                        if ($stack->isEmpty()) {
295 29
                            yield $createEvent(Event::DOCUMENT_END);
296
                        }
297 35
                        break;
298
299
                    default:
300
                        throw ParserException::unexpectedToken($token);
301
                }
302
303 35
                continue;
304
            }
305
        }
306 44
    }
307
308
    /**
309
     * @return iterable|Tokenizer|Token[]
310
     */
311 44
    public function tokens(): iterable
312
    {
313 44
        return new Tokenizer($this->buffer);
314
    }
315
316
    /**
317
     * @param string $eventId
318
     * @param        $value
319
     * @param Stack  $stack
320
     * @param int    $lineNumber
321
     * @param int    $charNumber
322
     * @return Event
323
     */
324 44
    protected function createEvent(string $eventId, $value, Stack $stack, int $lineNumber, int $charNumber): Event
325
    {
326 44
        return new Event($eventId, $value, $stack, $lineNumber, $charNumber);
327
    }
328
}
329