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:
commit
da3f0f0b0c
2
.github/workflows/makefile.yml
vendored
2
.github/workflows/makefile.yml
vendored
@ -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
|
||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -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.
|
||||
|
3
Makefile
3
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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"}
|
||||
|
@ -115,6 +115,9 @@ final class Items implements \IteratorAggregate, PositionAware
|
||||
return new self($iterable, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
|
151
src/Parser.php
151
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 === '-';
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user