Skip to content

Commit

Permalink
Fixes #117
Browse files Browse the repository at this point in the history
  • Loading branch information
halaxa committed Dec 1, 2024
1 parent c889fdf commit 527ab49
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 111 deletions.
48 changes: 29 additions & 19 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ class Parser implements \IteratorAggregate, PositionAware
/** @var Traversable */
private $tokens;

/** @var Iterator<int, string> */
private $tokensIterator;

/** @var Generator */
private $generator;

Expand Down Expand Up @@ -89,13 +86,6 @@ public function __construct(Traversable $tokens, $jsonPointer = '', ?ItemDecoder
}

$this->tokens = $tokens;
if ($tokens instanceof IteratorAggregate) {
$this->tokensIterator = $tokens->getIterator();
} elseif ($tokens instanceof Iterator) {
$this->tokensIterator = $tokens;
} else {
throw new InvalidArgumentException('$tokens must be either an instance of Iterator or IteratorAggregate.');
}

if ($jsonDecoder instanceof StringOnlyDecoder) {
$this->jsonDecoder = $jsonDecoder;
Expand All @@ -119,10 +109,12 @@ private function buildPaths(array $jsonPointers): array
#[\ReturnTypeWillChange]
public function getIterator(): Generator
{
if ( ! $this->generator) {
$this->generator = $this->createGenerator();
if ($this->generator && $this->tokens instanceof ResumableTokensProxy) {
throw new JsonMachineException('Nested RecursiveTokens cannot be iterated more than once.');
}

$this->generator = $this->createGenerator();

return $this->generator;
}

Expand Down Expand Up @@ -156,10 +148,10 @@ private function createGenerator(): Generator
$jsonPointerPath = [];
$iteratorLevel = 0;

// local variables for faster name lookups
$tokens = $this->tokensIterator;
/** @var Iterator */
$tokensIterator = $this->resolveTokensIterator();

foreach ($tokens as $token) {
foreach ($tokensIterator as $token) {
if ($currentPathChanged) {
$currentPathChanged = false;
$jsonPointerPath = $this->getMatchingJsonPointerPath();
Expand Down Expand Up @@ -196,7 +188,7 @@ private function createGenerator(): Generator
) {
if ($this->recursive && ($token == '{' || $token == '[')) {
$jsonValue = new self(
new ResumableIteratorAggregateProxy($this->tokens), // could single shared instance work?
new ResumableTokensProxy($this->tokens, $tokensIterator),
'',
$this->jsonDecoder,
true
Expand Down Expand Up @@ -335,10 +327,12 @@ private function createGenerator(): Generator

public function ensureIterationComplete(): void
{
$generator = $this->getIterator();
if ( ! $this->generator) {
$this->getIterator();
}

while ($generator->valid()) {
$generator->next();
while ($this->generator->valid()) {
$this->generator->next();
}
}

Expand Down Expand Up @@ -495,4 +489,20 @@ private function pathMatchesPointer($pathToken, string $pointerToken): bool

return is_int($pathToken) && $pointerToken === '-';
}

/**
* @throws InvalidArgumentException
*/
private function resolveTokensIterator()
{
if ($this->tokens instanceof IteratorAggregate) {
$tokensIterator = $this->tokens->getIterator();
} elseif ($this->tokens instanceof Iterator) {
$tokensIterator = $this->tokens;
} else {
throw new InvalidArgumentException('$tokens must be either an instance of Iterator or IteratorAggregate.');
}

return $tokensIterator;
}
}
15 changes: 1 addition & 14 deletions src/RecursiveItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace JsonMachine;

use Exception;
use Generator;
use Iterator;
use IteratorAggregate;
Expand Down Expand Up @@ -136,7 +135,6 @@ public function valid(): bool
public function rewind(): void
{
$this->parserIterator = toIterator($this->parser->getIterator());
$this->parserIterator->rewind();
}

public function hasChildren(): bool
Expand Down Expand Up @@ -185,21 +183,10 @@ public function advanceToKey($key)
/**
* Recursively materializes this iterator level to array.
* Moves its internal pointer to the end.
*
* @throws JsonMachineException
*/
public function toArray(): array
{
try {
/** @throws Exception */
$this->rewind();
} catch (Exception $e) {
if (false !== strpos($e->getMessage(), 'generator')) {
throw new JsonMachineException(
'Method toArray() can only be called before any items in the collection have been accessed.'
);
}
}
$this->rewind();

return self::toArrayRecursive($this);
}
Expand Down
56 changes: 0 additions & 56 deletions src/ResumableIteratorAggregateProxy.php

This file was deleted.

49 changes: 49 additions & 0 deletions src/ResumableTokensProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace JsonMachine;

use IteratorAggregate;

/**
* Allows to resume iteration of the inner IteratorAggregate via foreach, which would be otherwise impossible as
* foreach implicitly calls reset(). This Iterator does not pass the reset() call to the inner Iterator thus enabling
* to follow up on a previous iteation.
*/
class ResumableTokensProxy implements IteratorAggregate, PositionAware
{
/** @var \Iterator */
private $generator;

/** @var \Traversable|PositionAware */
private $tokens;

public function __construct(\Traversable $tokens, \Iterator $tokensGenerator)
{
$this->generator = $tokensGenerator;
$this->tokens = $tokens;
}

public function getIterator(): \Traversable
{
$generator = $this->generator;
while ($generator->valid()) {
yield $generator->key() => $generator->current();
$generator->next();
}
}

public function __call($name, $arguments)
{
return $this->generator->$name(...$arguments);
}

/**
* Returns JSON bytes read so far.
*/
public function getPosition()
{
return $this->tokens->getPosition();
}
}
9 changes: 1 addition & 8 deletions src/Tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ class Tokens implements \IteratorAggregate, PositionAware
/** @var iterable */
private $jsonChunks;

/** @var Generator */
private $generator;

/**
* @param iterable<string> $jsonChunks
*/
Expand All @@ -28,11 +25,7 @@ public function __construct($jsonChunks)
#[\ReturnTypeWillChange]
public function getIterator()
{
if ( ! $this->generator) {
$this->generator = $this->createGenerator();
}

return $this->generator;
return $this->createGenerator();
}

private function createGenerator(): Generator
Expand Down
8 changes: 8 additions & 0 deletions test/JsonMachineTest/ItemsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace JsonMachineTest;

use JsonMachine\Items;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
use JsonMachine\JsonDecoder\PassThruDecoder;

/**
Expand Down Expand Up @@ -146,4 +147,11 @@ public function testCountViaIteratorCount()

$this->assertSame(3, iterator_count($items));
}

public function testIssue117ItemsCanBeIteratedMoreThanOnce()
{
$rows = Items::fromFile(__DIR__.'/ItemsTest.json', ['decoder' => new ExtJsonDecoder(true)]);
$this->assertSame(1, iterator_count($rows));
$this->assertSame(1, iterator_count($rows));
}
}
5 changes: 3 additions & 2 deletions test/JsonMachineTest/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -628,14 +628,15 @@ public function testRecursiveParserDoesNotRequireChildParserToBeIteratedToTheEnd
$this->assertInstanceOf(Traversable::class, $array[1]);
$this->assertSame(4, $array[2]);

$this->expectExceptionMessage('generator');
$this->expectException(JsonMachineException::class);
$this->expectExceptionMessage('once');
iterator_to_array($array[1]);
}

public function data_testRecursiveParserDoesNotRequireChildParserToBeIteratedToTheEndByUser()
{
return [
['[1,[{},2,3],4]'],
['[1,{"x": 1,"y": 2,"z": 3},4]'],
['[1,[[],2,3],4]'],
['[1,[{"key": "value"},2,3],4]'],
['[1,[[null, true, "string"],2,3],4]'],
Expand Down
17 changes: 5 additions & 12 deletions test/JsonMachineTest/RecursiveItemsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Iterator;
use IteratorAggregate;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
use JsonMachine\RecursiveItems;

/**
Expand Down Expand Up @@ -188,20 +189,12 @@ public function testHasChildrenFollowsIterators()
$this->assertSame([false, true, false], $result);
}

public function testToArrayThrowsMeaningfulErrorWhenIteratorIsAlreadyOpen()
public function testIssue117ItemsCanBeIteratedMoreThanOnce()
{
$generator = function () {
yield 'one' => 1;
yield 'two' => 2;
yield 'three' => 3;
};
$iterator = new RecursiveItems(toIteratorAggregate($generator()));

$iterator->rewind();
$iterator->next();
$rows = RecursiveItems::fromFile(__DIR__.'/ItemsTest.json', ['decoder' => new ExtJsonDecoder(true)]);

$this->expectExceptionMessage('toArray()');
$iterator->toArray();
$this->assertSame(1, iterator_count($rows));
$this->assertSame(1, iterator_count($rows));
}
}

Expand Down

0 comments on commit 527ab49

Please sign in to comment.