1
0
mirror of https://github.com/halaxa/json-machine.git synced 2025-01-17 04:58:16 +01:00

Merge branch 'master' into ext

This commit is contained in:
Filip Halaxa 2023-05-05 15:42:58 +02:00
commit da3f0f0b0c
10 changed files with 205 additions and 83 deletions

View File

@ -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

View File

@ -12,6 +12,28 @@ Nothing yet
<br>
## 1.1.3 - 2022-10-12
### Fixed
- Fix the parsing of nested sub-trees that use wildcards (#83). Thanks @cerbero90
<br>
## 1.1.2 - 2022-09-29
### Added
- PHP 8.2 support
### Fixed
- Meaningful error on invalid token. (#86)
- Added missing return type annotation. (#84)
<br>
## 1.1.1 - 2022-03-03
### Fixed
- Fixed warning when generating autoload classmap via composer.
<br>
## 1.1.0 - 2022-02-19
### Added
- Autoloading without Composer. Thanks @a-sync.

View File

@ -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

View File

@ -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

View File

@ -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"}

View File

@ -115,6 +115,9 @@ final class Items implements \IteratorAggregate, PositionAware
return new self($iterable, $options);
}
/**
* @return \Generator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{

View File

@ -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 === '-';
}
}

View File

@ -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));
}
}

View File

@ -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) {
}
}
}

View File

@ -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