Add option to wrap the parser

This commit is contained in:
Andrea Marco Sartori 2023-08-06 01:43:40 +02:00
parent 5ce3f0744f
commit ec84c4b703
6 changed files with 104 additions and 2 deletions

View File

@ -270,7 +270,7 @@ $array = JsonParser::parse($source)->pointers(['/results/0/gender', '/results/0/
### 🐼 Lazy pointers
JSON Parser only keeps one key and one value in memory at a time. However, if the value is a large array or object, it may be inefficient to keep it all in memory.
JSON Parser only keeps one key and one value in memory at a time. However, if the value is a large array or object, it may be inefficient or even impossible to keep it all in memory.
To solve this problem, we can use lazy pointers. These pointers recursively keep in memory only one key and one value at a time for any nested array or object.
@ -323,6 +323,23 @@ foreach (JsonParser::parse($source)->lazy() as $key => $value) {
}
```
We can recursively wrap any instance of `Cerbero\JsonParser\Tokens\Parser` by chaining `wrap()`. This lets us wrap lazy loaded JSON arrays and objects into classes with advanced functionalities, like mapping or filtering:
```php
$json = JsonParser::parse($source)
->wrap(fn (Parser $parser) => new MyWrapper(fn () => yield from $parser))
->lazy();
foreach ($json as $key => $value) {
// 1st iteration: $key === 'results', $value instanceof MyWrapper
foreach ($value as $nestedKey => $nestedValue) {
// 1st iteration: $nestedKey === 0, $nestedValue instanceof MyWrapper
// 2nd iteration: $nestedKey === 1, $nestedValue instanceof MyWrapper
// ...
}
}
```
Lazy pointers also have all the other functionalities of normal pointers: they accept callbacks, can be set one by one or all together, can be eager loaded into an array and can be mixed with normal pointers as well:
```php

View File

@ -246,4 +246,17 @@ final class JsonParser implements IteratorAggregate
return $this;
}
/**
* Set the logic to run for wrapping the parser
*
* @param Closure $callback
* @return self
*/
public function wrap(Closure $callback): self
{
$this->config->wrapper = $callback;
return $this;
}
}

View File

@ -71,8 +71,9 @@ final class Parser implements IteratorAggregate
/** @var string|int $key */
$key = $this->decoder->decode($state->tree->currentKey());
$value = $this->decoder->decode($state->value());
$wrapper = $value instanceof self ? ($this->config->wrapper)($value) : $value;
yield $key => $state->callPointer($value, $key);
yield $key => $state->callPointer($wrapper, $key);
$value instanceof self && $value->fastForward();
}

View File

@ -10,6 +10,7 @@ use Cerbero\JsonParser\Exceptions\DecodingException;
use Cerbero\JsonParser\Exceptions\SyntaxException;
use Cerbero\JsonParser\Pointers\Pointer;
use Cerbero\JsonParser\Pointers\Pointers;
use Cerbero\JsonParser\Tokens\Parser;
use Closure;
/**
@ -53,6 +54,13 @@ final class Config
*/
public Closure $onSyntaxError;
/**
* The callback to run for wrapping the parser.
*
* @var Closure
*/
public Closure $wrapper;
/**
* Instantiate the class
*
@ -63,6 +71,7 @@ final class Config
$this->pointers = new Pointers();
$this->onDecodingError = fn (DecodedValue $decoded) => throw new DecodingException($decoded);
$this->onSyntaxError = fn (SyntaxException $e) => throw $e;
$this->wrapper = fn (Parser $parser) => $parser;
}
/**

View File

@ -3,6 +3,8 @@
use Cerbero\JsonParser\Dataset;
use Cerbero\JsonParser\Decoders\SimdjsonDecoder;
use Cerbero\JsonParser\JsonParser;
use Cerbero\JsonParser\Tokens\Parser;
use Pest\Expectation;
use function Cerbero\JsonParser\parseJson;
@ -42,3 +44,9 @@ it('shows the progress while parsing', function () {
expect($parser->progress()->percentage())->toBe(100.0);
});
it('wraps the parser recursively', function (string $source) {
$json = JsonParser::parse($source)->wrap(fn (Parser $parser) => yield from $parser)->lazy();
expect($json)->traverse(fn (Expectation $value) => $value->toBeWrappedInto(Generator::class));
})->with([fixture('json/complex_array.json'), fixture('json/complex_array.json')]);

View File

@ -1,6 +1,7 @@
<?php
use Cerbero\JsonParser\Tokens\Parser;
use Pest\Expectation;
if (!function_exists('fixture')) {
/**
@ -15,6 +16,43 @@ if (!function_exists('fixture')) {
}
}
/**
* Expect the given sequence from a Traversable
* Temporary fix to sequence() until this PR is merged: https://github.com/pestphp/pest/pull/895
*
* @param mixed ...$callbacks
* @return Expectation
*/
expect()->extend('traverse', function (mixed ...$callbacks) {
if (! is_iterable($this->value)) {
throw new BadMethodCallException('Expectation value is not iterable.');
}
if (empty($callbacks)) {
throw new InvalidArgumentException('No sequence expectations defined.');
}
$index = $valuesCount = 0;
foreach ($this->value as $key => $value) {
$valuesCount++;
if ($callbacks[$index] instanceof Closure) {
$callbacks[$index](new self($value), new self($key));
} else {
(new self($value))->toEqual($callbacks[$index]);
}
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
}
if (count($callbacks) > $valuesCount) {
throw new OutOfRangeException('Sequence expectations are more than the iterable items');
}
return $this;
});
/**
* Expect that keys and values are parsed correctly
*
@ -74,4 +112,20 @@ expect()->extend('toLazyLoadRecursively', function (array $keys, array $expected
expect($key)->toBe($expectedKey)->and($value)->toLazyLoadRecursively($keys, $expected);
}
}
return $this;
});
/**
* Expect that all Parser instances are wrapped recursively
*
* @param string $wrapper
* @return Expectation
*/
expect()->extend('toBeWrappedInto', function (string $wrapper) {
return $this->when(is_object($this->value), fn (Expectation $value) => $value
->toBeInstanceOf($wrapper)
->not->toBeInstanceOf(Parser::class)
->traverse(fn (Expectation $value) => $value->toBeWrappedInto($wrapper))
);
});