Fix intersections among pointers with wildcards

This commit is contained in:
Andrea Marco Sartori 2023-03-08 16:54:25 +10:00
parent 7f3e1734ea
commit 66e57b4e79
6 changed files with 105 additions and 82 deletions

View File

@ -15,13 +15,6 @@ use Traversable;
*/
final class Parser implements IteratorAggregate
{
/**
* The JSON parsing state.
*
* @var State
*/
private State $state;
/**
* The decoder handling potential errors.
*
@ -37,7 +30,6 @@ final class Parser implements IteratorAggregate
*/
public function __construct(private Lexer $lexer, private Config $config)
{
$this->state = new State();
$this->decoder = new ConfigurableDecoder($config);
}
@ -59,28 +51,28 @@ final class Parser implements IteratorAggregate
*/
public function getIterator(): Traversable
{
$this->state->setPointers(...$this->config->pointers);
$state = new State(...$this->config->pointers);
foreach ($this->lexer as $token) {
if (!$token->matches($this->state->expectedToken)) {
if (!$token->matches($state->expectedToken)) {
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;
}
if ($this->state->hasBuffer()) {
if ($state->hasBuffer()) {
/** @var string|int $key */
$key = $this->decoder->decode($this->state->key());
$value = $this->decoder->decode($this->state->value());
$key = $this->decoder->decode($state->key());
$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;
}
}

View File

@ -19,6 +19,13 @@ final class 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.
*
@ -34,6 +41,17 @@ final class Pointers
public function __construct(Pointer ...$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
{
if (count($this->pointers) < 2) {
return $this->matching;
}
$pointers = [];
$originalTree = $tree->original();
foreach ($this->pointers as $pointer) {
foreach ($tree->original() as $depth => $key) {
$referenceTokens = $pointer->referenceTokens();
foreach ($originalTree as $depth => $key) {
if (!$pointer->depthMatchesKey($depth, $key)) {
continue 2;
} elseif (!isset($pointers[$depth])) {
} elseif (!isset($pointers[$depth]) || $referenceTokens == $originalTree) {
$pointers[$depth] = $pointer;
}
}
}
return end($pointers) ?: $this->pointers[0];
return $this->matching = end($pointers) ?: $this->matching;
}
/**
* Mark the given pointer as found
*
* @param Pointer $pointer
* @return void
*/
public function markAsFound(Pointer $pointer): void
public function markAsFound(): void
{
if (!$pointer->wasFound) {
$this->found[(string) $pointer] = $pointer->wasFound = true;
if (!$this->matching->wasFound) {
$this->found[(string) $this->matching] = $this->matching->wasFound = true;
}
}
@ -79,16 +103,6 @@ final class Pointers
*/
public function wereFound(): bool
{
return $this->count() == count($this->found);
}
/**
* Retrieve the number of JSON pointers
*
* @return int
*/
public function count(): int
{
return count($this->pointers);
return count($this->pointers) == count($this->found) && !empty($this->pointers);
}
}

View File

@ -27,13 +27,6 @@ final class State
*/
private Pointers $pointers;
/**
* The JSON pointer matching the tree.
*
* @var Pointer
*/
private Pointer $pointer;
/**
* The JSON buffer.
*
@ -58,10 +51,12 @@ final class State
/**
* 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
{
return $this->pointer == ''
? $this->tree->depth() > $this->pointer->depth()
: $this->tree->depth() >= $this->pointer->depth();
return $this->pointers->matching() == ''
? $this->tree->depth() > $this->pointers->matching()->depth()
: $this->tree->depth() >= $this->pointers->matching()->depth();
}
/**
@ -96,19 +91,6 @@ final class State
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
*
@ -116,7 +98,7 @@ final class State
*/
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
{
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
{
$this->tree->changed = false;
$shouldTrackTree = $this->pointer == '' || $this->tree->depth() < $this->pointer->depth();
$shouldTrackTree = $this->pointers->matching() == '' || $this->tree->depth() < $this->pointers->matching()->depth();
if ($shouldTrackTree && $this->expectsKey) {
$this->tree->traverseKey($token);
} elseif ($shouldTrackTree && $token->isValue() && !$this->tree->inObject()) {
$this->tree->traverseArray($this->pointer->referenceTokens());
}
if ($this->tree->changed && $this->pointers->count() > 1) {
$this->pointer = $this->pointers->matchTree($this->tree);
$this->tree->traverseArray();
}
$shouldBuffer = $this->tree->depth() >= 0
&& $this->pointer->matchesTree($this->tree)
&& $this->pointers->matching()->matchesTree($this->tree)
&& ((!$this->expectsKey && $token->isValue()) || $this->treeIsDeep());
if ($shouldBuffer) {
$this->buffer .= $token;
$this->pointers->markAsFound($this->pointer);
$this->pointers->markAsFound();
}
$token->mutateState($this);

View File

@ -2,6 +2,8 @@
namespace Cerbero\JsonParser;
use Cerbero\JsonParser\Pointers\Pointers;
use function count;
/**
@ -39,11 +41,13 @@ final class Tree
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
@ -119,26 +123,27 @@ final class Tree
$this->original[$this->depth] = $trimmedKey;
$this->wildcarded[$this->depth] = $trimmedKey;
$this->changed = true;
$this->pointers->matchTree($this);
}
/**
* Traverse an array
*
* @param string[] $referenceTokens
* @return void
*/
public function traverseArray(array $referenceTokens): void
public function traverseArray(): void
{
$referenceToken = $referenceTokens[$this->depth] ?? null;
$index = $this->original[$this->depth] ?? null;
$this->original[$this->depth] = is_int($index) ? $index + 1 : 0;
$this->wildcarded[$this->depth] = $referenceToken == '-' ? '-' : $this->original[$this->depth];
$this->changed = true;
$this->original[$this->depth] = $index = is_int($index) ? $index + 1 : 0;
if (count($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);
}
}

View File

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

View File

@ -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) {
expect(JsonParser::parse($json)->pointers($pointers))->toPointTo($parsed);
})->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());