mirror of
https://github.com/cerbero90/json-parser.git
synced 2025-01-17 13:08:16 +01:00
Fix intersections among pointers with wildcards
This commit is contained in:
parent
7f3e1734ea
commit
66e57b4e79
@ -15,13 +15,6 @@ use Traversable;
|
|||||||
*/
|
*/
|
||||||
final class Parser implements IteratorAggregate
|
final class Parser implements IteratorAggregate
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The JSON parsing state.
|
|
||||||
*
|
|
||||||
* @var State
|
|
||||||
*/
|
|
||||||
private State $state;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The decoder handling potential errors.
|
* The decoder handling potential errors.
|
||||||
*
|
*
|
||||||
@ -37,7 +30,6 @@ final class Parser implements IteratorAggregate
|
|||||||
*/
|
*/
|
||||||
public function __construct(private Lexer $lexer, private Config $config)
|
public function __construct(private Lexer $lexer, private Config $config)
|
||||||
{
|
{
|
||||||
$this->state = new State();
|
|
||||||
$this->decoder = new ConfigurableDecoder($config);
|
$this->decoder = new ConfigurableDecoder($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,28 +51,28 @@ final class Parser implements IteratorAggregate
|
|||||||
*/
|
*/
|
||||||
public function getIterator(): Traversable
|
public function getIterator(): Traversable
|
||||||
{
|
{
|
||||||
$this->state->setPointers(...$this->config->pointers);
|
$state = new State(...$this->config->pointers);
|
||||||
|
|
||||||
foreach ($this->lexer as $token) {
|
foreach ($this->lexer as $token) {
|
||||||
if (!$token->matches($this->state->expectedToken)) {
|
if (!$token->matches($state->expectedToken)) {
|
||||||
throw new SyntaxException($token, $this->lexer->position());
|
throw new SyntaxException($token, $this->lexer->position());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->state->mutateByToken($token);
|
$state->mutateByToken($token);
|
||||||
|
|
||||||
if (!$token->endsChunk() || $this->state->treeIsDeep()) {
|
if (!$token->endsChunk() || $state->treeIsDeep()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->state->hasBuffer()) {
|
if ($state->hasBuffer()) {
|
||||||
/** @var string|int $key */
|
/** @var string|int $key */
|
||||||
$key = $this->decoder->decode($this->state->key());
|
$key = $this->decoder->decode($state->key());
|
||||||
$value = $this->decoder->decode($this->state->value());
|
$value = $this->decoder->decode($state->value());
|
||||||
|
|
||||||
yield $key => $this->state->callPointer($value, $key);
|
yield $key => $state->callPointer($value, $key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->state->canStopParsing()) {
|
if ($state->canStopParsing()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,13 @@ final class Pointers
|
|||||||
*/
|
*/
|
||||||
private array $pointers;
|
private array $pointers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The JSON pointer matching with the current tree.
|
||||||
|
*
|
||||||
|
* @var Pointer
|
||||||
|
*/
|
||||||
|
private Pointer $matching;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of pointers that were found within the JSON.
|
* The list of pointers that were found within the JSON.
|
||||||
*
|
*
|
||||||
@ -34,6 +41,17 @@ final class Pointers
|
|||||||
public function __construct(Pointer ...$pointers)
|
public function __construct(Pointer ...$pointers)
|
||||||
{
|
{
|
||||||
$this->pointers = $pointers;
|
$this->pointers = $pointers;
|
||||||
|
$this->matching = $pointers[0] ?? new Pointer('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the pointer matching the current tree
|
||||||
|
*
|
||||||
|
* @return Pointer
|
||||||
|
*/
|
||||||
|
public function matching(): Pointer
|
||||||
|
{
|
||||||
|
return $this->matching;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,31 +62,37 @@ final class Pointers
|
|||||||
*/
|
*/
|
||||||
public function matchTree(Tree $tree): Pointer
|
public function matchTree(Tree $tree): Pointer
|
||||||
{
|
{
|
||||||
|
if (count($this->pointers) < 2) {
|
||||||
|
return $this->matching;
|
||||||
|
}
|
||||||
|
|
||||||
$pointers = [];
|
$pointers = [];
|
||||||
|
$originalTree = $tree->original();
|
||||||
|
|
||||||
foreach ($this->pointers as $pointer) {
|
foreach ($this->pointers as $pointer) {
|
||||||
foreach ($tree->original() as $depth => $key) {
|
$referenceTokens = $pointer->referenceTokens();
|
||||||
|
|
||||||
|
foreach ($originalTree as $depth => $key) {
|
||||||
if (!$pointer->depthMatchesKey($depth, $key)) {
|
if (!$pointer->depthMatchesKey($depth, $key)) {
|
||||||
continue 2;
|
continue 2;
|
||||||
} elseif (!isset($pointers[$depth])) {
|
} elseif (!isset($pointers[$depth]) || $referenceTokens == $originalTree) {
|
||||||
$pointers[$depth] = $pointer;
|
$pointers[$depth] = $pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return end($pointers) ?: $this->pointers[0];
|
return $this->matching = end($pointers) ?: $this->matching;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the given pointer as found
|
* Mark the given pointer as found
|
||||||
*
|
*
|
||||||
* @param Pointer $pointer
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function markAsFound(Pointer $pointer): void
|
public function markAsFound(): void
|
||||||
{
|
{
|
||||||
if (!$pointer->wasFound) {
|
if (!$this->matching->wasFound) {
|
||||||
$this->found[(string) $pointer] = $pointer->wasFound = true;
|
$this->found[(string) $this->matching] = $this->matching->wasFound = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,16 +103,6 @@ final class Pointers
|
|||||||
*/
|
*/
|
||||||
public function wereFound(): bool
|
public function wereFound(): bool
|
||||||
{
|
{
|
||||||
return $this->count() == count($this->found);
|
return count($this->pointers) == count($this->found) && !empty($this->pointers);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the number of JSON pointers
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function count(): int
|
|
||||||
{
|
|
||||||
return count($this->pointers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,6 @@ final class State
|
|||||||
*/
|
*/
|
||||||
private Pointers $pointers;
|
private Pointers $pointers;
|
||||||
|
|
||||||
/**
|
|
||||||
* The JSON pointer matching the tree.
|
|
||||||
*
|
|
||||||
* @var Pointer
|
|
||||||
*/
|
|
||||||
private Pointer $pointer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The JSON buffer.
|
* The JSON buffer.
|
||||||
*
|
*
|
||||||
@ -58,10 +51,12 @@ final class State
|
|||||||
/**
|
/**
|
||||||
* Instantiate the class.
|
* Instantiate the class.
|
||||||
*
|
*
|
||||||
|
* @param Pointer ...$pointers
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct(Pointer ...$pointers)
|
||||||
{
|
{
|
||||||
$this->tree = new Tree();
|
$this->pointers = new Pointers(...$pointers);
|
||||||
|
$this->tree = new Tree($this->pointers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,9 +76,9 @@ final class State
|
|||||||
*/
|
*/
|
||||||
public function treeIsDeep(): bool
|
public function treeIsDeep(): bool
|
||||||
{
|
{
|
||||||
return $this->pointer == ''
|
return $this->pointers->matching() == ''
|
||||||
? $this->tree->depth() > $this->pointer->depth()
|
? $this->tree->depth() > $this->pointers->matching()->depth()
|
||||||
: $this->tree->depth() >= $this->pointer->depth();
|
: $this->tree->depth() >= $this->pointers->matching()->depth();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,19 +91,6 @@ final class State
|
|||||||
return $this->tree->currentKey();
|
return $this->tree->currentKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set and match the given pointers
|
|
||||||
*
|
|
||||||
* @param Pointer ...$pointers
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setPointers(Pointer ...$pointers): void
|
|
||||||
{
|
|
||||||
$this->pointers = new Pointers(...$pointers ?: [new Pointer('')]);
|
|
||||||
|
|
||||||
$this->pointer = $this->pointers->matchTree($this->tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the parser can stop parsing
|
* Determine whether the parser can stop parsing
|
||||||
*
|
*
|
||||||
@ -116,7 +98,7 @@ final class State
|
|||||||
*/
|
*/
|
||||||
public function canStopParsing(): bool
|
public function canStopParsing(): bool
|
||||||
{
|
{
|
||||||
return $this->pointers->wereFound() && !$this->pointer->includesTree($this->tree);
|
return $this->pointers->wereFound() && !$this->pointers->matching()->includesTree($this->tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,7 +110,7 @@ final class State
|
|||||||
*/
|
*/
|
||||||
public function callPointer(mixed $value, mixed $key): mixed
|
public function callPointer(mixed $value, mixed $key): mixed
|
||||||
{
|
{
|
||||||
return $this->pointer->call($value, $key);
|
return $this->pointers->matching()->call($value, $key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,26 +121,21 @@ final class State
|
|||||||
*/
|
*/
|
||||||
public function mutateByToken(Token $token): void
|
public function mutateByToken(Token $token): void
|
||||||
{
|
{
|
||||||
$this->tree->changed = false;
|
$shouldTrackTree = $this->pointers->matching() == '' || $this->tree->depth() < $this->pointers->matching()->depth();
|
||||||
$shouldTrackTree = $this->pointer == '' || $this->tree->depth() < $this->pointer->depth();
|
|
||||||
|
|
||||||
if ($shouldTrackTree && $this->expectsKey) {
|
if ($shouldTrackTree && $this->expectsKey) {
|
||||||
$this->tree->traverseKey($token);
|
$this->tree->traverseKey($token);
|
||||||
} elseif ($shouldTrackTree && $token->isValue() && !$this->tree->inObject()) {
|
} elseif ($shouldTrackTree && $token->isValue() && !$this->tree->inObject()) {
|
||||||
$this->tree->traverseArray($this->pointer->referenceTokens());
|
$this->tree->traverseArray();
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->tree->changed && $this->pointers->count() > 1) {
|
|
||||||
$this->pointer = $this->pointers->matchTree($this->tree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$shouldBuffer = $this->tree->depth() >= 0
|
$shouldBuffer = $this->tree->depth() >= 0
|
||||||
&& $this->pointer->matchesTree($this->tree)
|
&& $this->pointers->matching()->matchesTree($this->tree)
|
||||||
&& ((!$this->expectsKey && $token->isValue()) || $this->treeIsDeep());
|
&& ((!$this->expectsKey && $token->isValue()) || $this->treeIsDeep());
|
||||||
|
|
||||||
if ($shouldBuffer) {
|
if ($shouldBuffer) {
|
||||||
$this->buffer .= $token;
|
$this->buffer .= $token;
|
||||||
$this->pointers->markAsFound($this->pointer);
|
$this->pointers->markAsFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
$token->mutateState($this);
|
$token->mutateState($this);
|
||||||
|
27
src/Tree.php
27
src/Tree.php
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Cerbero\JsonParser;
|
namespace Cerbero\JsonParser;
|
||||||
|
|
||||||
|
use Cerbero\JsonParser\Pointers\Pointers;
|
||||||
|
|
||||||
use function count;
|
use function count;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,11 +41,13 @@ final class Tree
|
|||||||
private int $depth = -1;
|
private int $depth = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the tree changed.
|
* Instantiate the class.
|
||||||
*
|
*
|
||||||
* @var bool
|
* @param Pointers $pointers
|
||||||
*/
|
*/
|
||||||
public bool $changed = false;
|
public function __construct(private Pointers $pointers)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the original JSON tree
|
* Retrieve the original JSON tree
|
||||||
@ -119,26 +123,27 @@ final class Tree
|
|||||||
|
|
||||||
$this->original[$this->depth] = $trimmedKey;
|
$this->original[$this->depth] = $trimmedKey;
|
||||||
$this->wildcarded[$this->depth] = $trimmedKey;
|
$this->wildcarded[$this->depth] = $trimmedKey;
|
||||||
$this->changed = true;
|
$this->pointers->matchTree($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverse an array
|
* Traverse an array
|
||||||
*
|
*
|
||||||
* @param string[] $referenceTokens
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function traverseArray(array $referenceTokens): void
|
public function traverseArray(): void
|
||||||
{
|
{
|
||||||
$referenceToken = $referenceTokens[$this->depth] ?? null;
|
|
||||||
$index = $this->original[$this->depth] ?? null;
|
$index = $this->original[$this->depth] ?? null;
|
||||||
|
$this->original[$this->depth] = $index = is_int($index) ? $index + 1 : 0;
|
||||||
$this->original[$this->depth] = is_int($index) ? $index + 1 : 0;
|
|
||||||
$this->wildcarded[$this->depth] = $referenceToken == '-' ? '-' : $this->original[$this->depth];
|
|
||||||
$this->changed = true;
|
|
||||||
|
|
||||||
if (count($this->original) > $this->depth + 1) {
|
if (count($this->original) > $this->depth + 1) {
|
||||||
array_splice($this->original, $this->depth + 1);
|
array_splice($this->original, $this->depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$referenceTokens = $this->pointers->matchTree($this)->referenceTokens();
|
||||||
|
$this->wildcarded[$this->depth] = ($referenceTokens[$this->depth] ?? null) == '-' ? '-' : $index;
|
||||||
|
|
||||||
|
if (count($this->wildcarded) > $this->depth + 1) {
|
||||||
array_splice($this->wildcarded, $this->depth + 1);
|
array_splice($this->wildcarded, $this->depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,37 @@ final class Dataset
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the dataset to test intersecting pointers with wildcards
|
||||||
|
*
|
||||||
|
* @return Generator
|
||||||
|
*/
|
||||||
|
public static function forIntersectingPointersWithWildcards(): Generator
|
||||||
|
{
|
||||||
|
$json = fixture('json/complex_object.json');
|
||||||
|
|
||||||
|
$pointers = [
|
||||||
|
'/topping/6/type' => fn (string $value) => "$value @ /topping/6/type",
|
||||||
|
'/topping/-/type' => fn (string $value) => "$value @ /topping/-/type",
|
||||||
|
'/topping/0/type' => fn (string $value) => "$value @ /topping/0/type",
|
||||||
|
'/topping/2/type' => fn (string $value) => "$value @ /topping/2/type",
|
||||||
|
];
|
||||||
|
|
||||||
|
$parsed = [
|
||||||
|
'type' => [
|
||||||
|
'None @ /topping/0/type',
|
||||||
|
'Glazed @ /topping/-/type',
|
||||||
|
'Sugar @ /topping/2/type',
|
||||||
|
'Powdered Sugar @ /topping/-/type',
|
||||||
|
'Chocolate with Sprinkles @ /topping/-/type',
|
||||||
|
'Chocolate @ /topping/-/type',
|
||||||
|
'Maple @ /topping/6/type',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [$json, $pointers, $parsed];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the dataset to test syntax errors
|
* Retrieve the dataset to test syntax errors
|
||||||
*
|
*
|
||||||
|
@ -17,3 +17,7 @@ it('supports single JSON pointers', function (string $json, string $pointer, arr
|
|||||||
it('supports multiple JSON pointers', function (string $json, array $pointers, array $parsed) {
|
it('supports multiple JSON pointers', function (string $json, array $pointers, array $parsed) {
|
||||||
expect(JsonParser::parse($json)->pointers($pointers))->toPointTo($parsed);
|
expect(JsonParser::parse($json)->pointers($pointers))->toPointTo($parsed);
|
||||||
})->with(Dataset::forMultiplePointers());
|
})->with(Dataset::forMultiplePointers());
|
||||||
|
|
||||||
|
it('can intersect pointers with wildcards', function (string $json, array $pointers, array $parsed) {
|
||||||
|
expect(JsonParser::parse($json)->pointers($pointers))->toPointTo($parsed);
|
||||||
|
})->with(Dataset::forIntersectingPointersWithWildcards());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user