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
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
27
src/Tree.php
27
src/Tree.php
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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());
|
||||
|
Loading…
x
Reference in New Issue
Block a user