diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml
index 9332bee..82eba84 100644
--- a/.github/workflows/makefile.yml
+++ b/.github/workflows/makefile.yml
@@ -17,6 +17,8 @@ jobs:
- uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
+ with:
+ key: docker-image-version-1-{hash}
- name: Complete Makefile build
run: make build
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c5e293..a275597 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,28 @@ Nothing yet
+## 1.1.3 - 2022-10-12
+### Fixed
+- Fix the parsing of nested sub-trees that use wildcards (#83). Thanks @cerbero90
+
+
+
+## 1.1.2 - 2022-09-29
+### Added
+- PHP 8.2 support
+
+### Fixed
+- Meaningful error on invalid token. (#86)
+- Added missing return type annotation. (#84)
+
+
+
+## 1.1.1 - 2022-03-03
+### Fixed
+- Fixed warning when generating autoload classmap via composer.
+
+
+
## 1.1.0 - 2022-02-19
### Added
- Autoloading without Composer. Thanks @a-sync.
diff --git a/Makefile b/Makefile
index 4b48f16..2e2ef5e 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,8 @@ define PHP_VERSIONS
"7.3 3.1.1"\
"7.4 3.1.1"\
"8.0 3.1.1"\
-"8.1 3.1.1"
+"8.1 3.1.1"\
+"8.2 3.2.0"
endef
define DOCKER_RUN
diff --git a/build/build-image.sh b/build/build-image.sh
index 9fb715a..d9e1835 100755
--- a/build/build-image.sh
+++ b/build/build-image.sh
@@ -26,10 +26,13 @@ printf "
g++ \
libtool \
make \
+ bash \
+ linux-headers \
&& wget http://pear.php.net/go-pear.phar && php go-pear.phar \
&& pecl install xdebug-$XDEBUG_VERSION \
&& docker-php-ext-enable xdebug \
- && wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer \
+ && docker-php-ext-enable opcache \
+ && wget https://getcomposer.org/download/2.2.18/composer.phar -O /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer
" | docker build --quiet --tag "$CONTAINER_NAME" - > /dev/null
diff --git a/composer.json b/composer.json
index 308f52f..8d8ff16 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
"tests-coverage": "build/composer-update.sh && XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover clover.xml",
"cs-check": "build/composer-update.sh && vendor/bin/php-cs-fixer fix --dry-run --verbose --allow-risky=yes",
"cs-fix": "build/composer-update.sh && vendor/bin/php-cs-fixer fix --verbose --allow-risky=yes",
- "performance-tests": "php -n test/performance/testPerformance.php"
+ "performance-tests": "php -d xdebug.mode=off -d opcache.enable_cli=1 -d opcache.jit_buffer_size=100M test/performance/testPerformance.php"
},
"config": {
"lock": false,
@@ -32,7 +32,8 @@
"guzzlehttp/guzzle": "To run example with GuzzleHttp"
},
"autoload": {
- "psr-4": {"JsonMachine\\": "src/"}
+ "psr-4": {"JsonMachine\\": "src/"},
+ "exclude-from-classmap": ["src/autoloader.php"]
},
"autoload-dev": {
"psr-4": {"JsonMachineTest\\": "test/JsonMachineTest"}
diff --git a/src/Items.php b/src/Items.php
index dde47bb..1dfb958 100644
--- a/src/Items.php
+++ b/src/Items.php
@@ -115,6 +115,9 @@ final class Items implements \IteratorAggregate, PositionAware
return new self($iterable, $options);
}
+ /**
+ * @return \Generator
+ */
#[\ReturnTypeWillChange]
public function getIterator()
{
diff --git a/src/Parser.php b/src/Parser.php
index bfa82f2..8a18265 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -120,14 +120,15 @@ class Parser implements \IteratorAggregate, PositionAware
$isValue = ($tokenType | 23) == 23; // 23 = self::ANY_VALUE
if ( ! $inObject && $isValue && $currentLevel < $iteratorLevel) {
$currentPathChanged = ! $this->hasSingleJsonPointer;
- $currentPath[$currentLevel] = isset($currentPath[$currentLevel]) ? (string) (1 + (int) $currentPath[$currentLevel]) : '0';
+ $currentPath[$currentLevel] = isset($currentPath[$currentLevel]) ? $currentPath[$currentLevel] + 1 : 0;
$currentPathWildcard[$currentLevel] = preg_match('/^(?:\d+|-)$/S', $jsonPointerPath[$currentLevel]) ? '-' : $currentPath[$currentLevel];
- unset($currentPath[$currentLevel + 1], $currentPathWildcard[$currentLevel + 1], $stack[$currentLevel + 1]);
+ array_splice($currentPath, $currentLevel + 1);
+ array_splice($currentPathWildcard, $currentLevel + 1);
}
if (
- (
- $jsonPointerPath == $currentPath
- || $jsonPointerPath == $currentPathWildcard
+ ( // array_diff may be replaced with '==' when PHP 7 stops being supported
+ ! array_diff($jsonPointerPath, $currentPath)
+ || ! array_diff($jsonPointerPath, $currentPathWildcard)
)
&& (
$currentLevel > $iteratorLevel
@@ -156,7 +157,8 @@ class Parser implements \IteratorAggregate, PositionAware
$currentPathChanged = ! $this->hasSingleJsonPointer;
$currentPath[$currentLevel] = $referenceToken;
$currentPathWildcard[$currentLevel] = $referenceToken;
- unset($currentPath[$currentLevel + 1], $currentPathWildcard[$currentLevel + 1]);
+ array_splice($currentPath, $currentLevel + 1);
+ array_splice($currentPathWildcard, $currentLevel + 1);
}
continue 2; // a valid json chunk is not completed yet
}
@@ -231,11 +233,14 @@ class Parser implements \IteratorAggregate, PositionAware
}
unset($valueResult);
}
- if ($jsonPointerPath == $currentPath || $jsonPointerPath == $currentPathWildcard) {
+ if (
+ ! array_diff($jsonPointerPath, $currentPath)
+ || ! array_diff($jsonPointerPath, $currentPathWildcard)
+ ) {
if ( ! in_array($this->matchedJsonPointer, $pointersFound, true)) {
$pointersFound[] = $this->matchedJsonPointer;
}
- } elseif (count($pointersFound) == count($this->jsonPointers)) {
+ } elseif (count($pointersFound) == count($this->jsonPointers) && ! $this->inJsonPointer()) {
$subtreeEnded = true;
break;
}
@@ -259,77 +264,53 @@ class Parser implements \IteratorAggregate, PositionAware
private function tokenTypes()
{
- return [
- 'n' => self::SCALAR_CONST,
- 't' => self::SCALAR_CONST,
- 'f' => self::SCALAR_CONST,
- '-' => self::SCALAR_CONST,
- '0' => self::SCALAR_CONST,
- '1' => self::SCALAR_CONST,
- '2' => self::SCALAR_CONST,
- '3' => self::SCALAR_CONST,
- '4' => self::SCALAR_CONST,
- '5' => self::SCALAR_CONST,
- '6' => self::SCALAR_CONST,
- '7' => self::SCALAR_CONST,
- '8' => self::SCALAR_CONST,
- '9' => self::SCALAR_CONST,
- '"' => self::SCALAR_STRING,
- '{' => self::OBJECT_START,
- '}' => self::OBJECT_END,
- '[' => self::ARRAY_START,
- ']' => self::ARRAY_END,
- ',' => self::COMMA,
- ':' => self::COLON,
- ];
+ $allBytes = [];
+
+ foreach (range(0, 255) as $ord) {
+ $allBytes[chr($ord)] = 0;
+ }
+
+ $allBytes['n'] = self::SCALAR_CONST;
+ $allBytes['t'] = self::SCALAR_CONST;
+ $allBytes['f'] = self::SCALAR_CONST;
+ $allBytes['-'] = self::SCALAR_CONST;
+ $allBytes['0'] = self::SCALAR_CONST;
+ $allBytes['1'] = self::SCALAR_CONST;
+ $allBytes['2'] = self::SCALAR_CONST;
+ $allBytes['3'] = self::SCALAR_CONST;
+ $allBytes['4'] = self::SCALAR_CONST;
+ $allBytes['5'] = self::SCALAR_CONST;
+ $allBytes['6'] = self::SCALAR_CONST;
+ $allBytes['7'] = self::SCALAR_CONST;
+ $allBytes['8'] = self::SCALAR_CONST;
+ $allBytes['9'] = self::SCALAR_CONST;
+ $allBytes['"'] = self::SCALAR_STRING;
+ $allBytes['{'] = self::OBJECT_START;
+ $allBytes['}'] = self::OBJECT_END;
+ $allBytes['['] = self::ARRAY_START;
+ $allBytes[']'] = self::ARRAY_END;
+ $allBytes[','] = self::COMMA;
+ $allBytes[':'] = self::COLON;
+
+ return $allBytes;
}
private function getMatchingJsonPointerPath(): array
{
- $matchingPointer = key($this->paths);
+ $matchingPointerByIndex = [];
- if (count($this->paths) === 1) {
- $this->matchedJsonPointer = $matchingPointer;
-
- return $this->paths[$matchingPointer];
- }
-
- $currentPathLength = count($this->currentPath);
- $matchLength = -1;
-
- foreach ($this->paths as $jsonPointer => $path) {
- $matchingReferenceTokens = [];
-
- foreach ($path as $i => $referenceToken) {
- if (
- ! isset($this->currentPath[$i])
- || (
- $this->currentPath[$i] !== $referenceToken
- && ValidJsonPointers::wildcardify($this->currentPath[$i]) !== $referenceToken
- )
- ) {
- continue;
+ foreach ($this->paths as $jsonPointer => $referenceTokens) {
+ foreach ($this->currentPath as $index => $pathToken) {
+ if ( ! isset($referenceTokens[$index]) || ! $this->pathMatchesPointer($pathToken, $referenceTokens[$index])) {
+ continue 2;
+ } elseif ( ! isset($matchingPointerByIndex[$index])) {
+ $matchingPointerByIndex[$index] = $jsonPointer;
}
-
- $matchingReferenceTokens[$i] = $referenceToken;
- }
-
- if (empty($matchingReferenceTokens)) {
- continue;
- }
-
- $currentMatchLength = count($matchingReferenceTokens);
-
- if ($currentMatchLength > $matchLength) {
- $matchingPointer = $jsonPointer;
- $matchLength = $currentMatchLength;
- }
-
- if ($matchLength === $currentPathLength) {
- break;
}
}
+ $matchingPointer = end($matchingPointerByIndex) ?: key($this->paths);
+
$this->matchedJsonPointer = $matchingPointer;
return $this->paths[$matchingPointer];
@@ -392,11 +373,39 @@ class Parser implements \IteratorAggregate, PositionAware
private static function pathToJsonPointer(array $path): string
{
$encodedParts = array_map(function ($addressPart) {
- return str_replace(['~', '/'], ['~0', '~1'], $addressPart);
+ return str_replace(['~', '/'], ['~0', '~1'], (string) $addressPart);
}, $path);
array_unshift($encodedParts, '');
return implode('/', $encodedParts);
}
+
+ /**
+ * Determine whether the current position is within one of the JSON pointers.
+ */
+ private function inJsonPointer(): bool
+ {
+ $jsonPointerPath = $this->paths[$this->matchedJsonPointer];
+
+ if (($firstNest = array_search('-', $jsonPointerPath)) === false) {
+ return false;
+ }
+
+ return array_slice($jsonPointerPath, 0, $firstNest) == array_slice($this->currentPath, 0, $firstNest);
+ }
+
+ /**
+ * Determine whether the given path reference token matches the provided JSON pointer reference token.
+ *
+ * @param string|int $pathToken
+ */
+ private function pathMatchesPointer($pathToken, string $pointerToken): bool
+ {
+ if ($pointerToken === (string) $pathToken) {
+ return true;
+ }
+
+ return is_int($pathToken) && $pointerToken === '-';
+ }
}
diff --git a/test/JsonMachineTest/ItemsTest.php b/test/JsonMachineTest/ItemsTest.php
index d0d06d7..9c4ab18 100644
--- a/test/JsonMachineTest/ItemsTest.php
+++ b/test/JsonMachineTest/ItemsTest.php
@@ -139,4 +139,11 @@ class ItemsTest extends \PHPUnit_Framework_TestCase
$this->assertSame(['/one', '/two'], $items->getJsonPointers());
}
+
+ public function testCountViaIteratorCount()
+ {
+ $items = Items::fromIterable(['{"results":', '[1,2,3]}'], ['pointer' => ['/results']]);
+
+ $this->assertSame(3, iterator_count($items));
+ }
}
diff --git a/test/JsonMachineTest/ParserTest.php b/test/JsonMachineTest/ParserTest.php
index 24d5125..6065450 100644
--- a/test/JsonMachineTest/ParserTest.php
+++ b/test/JsonMachineTest/ParserTest.php
@@ -124,7 +124,7 @@ class ParserTest extends \PHPUnit_Framework_TestCase
return [
'non existing pointer' => ['{}', '/not/found'],
"empty string should not match '0'" => ['{"0":[]}', '/'],
- 'empty string should not match 0' => ['[[]]', '/'],
+ 'empty string should not match 0 index' => ['[[]]', '/'],
'0 should not match empty string' => ['{"":[]}', '/0'],
];
}
@@ -281,14 +281,64 @@ class ParserTest extends \PHPUnit_Framework_TestCase
$parser = $this->createParser($json, '/zero/-/two/-/three');
- $i = 0;
- $expectedKey = 'three';
- $expectedValues = [1, 2, 3];
+ $actual = [];
+ $expected = ['three' => [1, 2, 3]];
foreach ($parser as $key => $value) {
- $this->assertSame($expectedKey, $key);
- $this->assertSame($expectedValues[$i++], $value);
+ $actual[$key][] = $value;
}
+
+ $this->assertSame($expected, $actual);
+ }
+
+ public function testGeneratorYieldsNestedValuesOfMultiplePaths()
+ {
+ $json = '
+ {
+ "zero": [
+ {
+ "one": "hello",
+ "two": [
+ {
+ "three": 1
+ }
+ ],
+ "four": [
+ {
+ "five": "ignored"
+ }
+ ]
+ },
+ {
+ "one": "bye",
+ "two": [
+ {
+ "three": 2
+ },
+ {
+ "three": 3
+ }
+ ],
+ "four": [
+ {
+ "five": "ignored"
+ }
+ ]
+ }
+ ]
+ }
+ ';
+
+ $parser = $this->createParser($json, ['/zero/-/one', '/zero/-/two/-/three']);
+
+ $actual = [];
+ $expected = ['one' => ['hello', 'bye'], 'three' => [1, 2, 3]];
+
+ foreach ($parser as $key => $value) {
+ $actual[$key][] = $value;
+ }
+
+ $this->assertSame($expected, $actual);
}
private function createParser($json, $jsonPointer = '')
@@ -466,4 +516,14 @@ class ParserTest extends \PHPUnit_Framework_TestCase
$this->expectException(JsonMachineException::class);
$parser->getPosition();
}
+
+ public function testThrowsMeaningfulErrorOnIncorrectTokens()
+ {
+ $parser = new Parser(new Tokens(['[$P]']));
+
+ $this->expectException(SyntaxErrorException::class);
+
+ foreach ($parser as $index => $item) {
+ }
+ }
}
diff --git a/test/performance/testPerformance.php b/test/performance/testPerformance.php
index 8ab81d7..042ea51 100644
--- a/test/performance/testPerformance.php
+++ b/test/performance/testPerformance.php
@@ -9,8 +9,22 @@ use JsonMachine\Parser;
require_once __DIR__.'/../../vendor/autoload.php';
-if (in_array('xdebug', get_loaded_extensions())) {
- trigger_error('Xdebug enabled. Results may be affected.', E_USER_WARNING);
+if ( ! ini_get('xdebug.mode')) {
+ echo "Xdebug disabled\n";
+} else {
+ echo "Xdebug enabled\n";
+}
+
+if ( ! function_exists('opcache_get_status')) {
+ echo "Opcache disabled\n";
+ echo "JIT disabled\n";
+} else {
+ echo "Opcache enabled\n";
+ if (opcache_get_status()['jit']['enabled']) {
+ echo "JIT enabled\n";
+ } else {
+ echo "JIT disabled\n";
+ }
}
ini_set('memory_limit', '-1'); // for json_decode use case