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