Merge branch 'feature/wrapper' into develop

This commit is contained in:
Andrea Marco Sartori 2023-08-06 17:29:37 +02:00
commit bf58862b96
10 changed files with 421 additions and 66 deletions

8
.gitignore vendored
View File

@ -1,7 +1,9 @@
build
composer.lock
profiling
vendor
phpcs.xml
phpunit.xml
.DS_Store
.phpunit.cache
.phpunit.result.cache
composer.lock
phpcs.xml
phpunit.xml

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,25 @@ 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
// ...
}
}
```
> If your wrapper class implements the method `toArray()`, such method will be called when eager loading sub-trees into an array.
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();
}
@ -112,13 +113,21 @@ final class Parser implements IteratorAggregate
*/
public function toArray(): array
{
$index = 0;
$array = [];
$hasWildcards = false;
foreach ($this as $key => $value) {
$array[$key] = $value instanceof self ? $value->toArray() : $value;
if (isset($array[$index][$key])) {
$index++;
$hasWildcards = true;
}
$turnsIntoArray = is_object($value) && method_exists($value, 'toArray');
$array[$index][$key] = $turnsIntoArray ? $value->toArray() : $value;
}
return $array;
return $hasWildcards || empty($array) ? $array : $array[0];
}
/**

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

@ -54,16 +54,6 @@ final class State
$this->tree = new Tree($pointers);
}
/**
* Retrieve the JSON tree
*
* @return Tree
*/
public function tree(): Tree
{
return $this->tree;
}
/**
* Determine whether the parser can stop parsing
*

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))
);
});

View File

@ -4,44 +4,168 @@ return [
'complex_array' => [
'/-1,/-2' => [],
'/-/id,/-/batters/batter/-/type' => [
'id' => '0003',
'type' => 'Chocolate',
[
'id' => '0001',
'type' => 'Regular',
],
[
'type' => 'Chocolate',
],
[
'type' => 'Blueberry',
],
[
'type' => 'Devil\'s Food',
'id' => '0002',
],
[
'type' => 'Regular',
'id' => '0003',
],
[
'type' => 'Regular',
],
[
'type' => 'Chocolate',
],
],
'/-/name,/-/topping/-/type,/-/id' => [
'id' => '0003',
'name' => 'Old Fashioned',
'type' => 'Maple',
[
'id' => '0001',
'name' => 'Cake',
'type' => 'None',
],
[
'type' => 'Glazed',
],
[
'type' => 'Sugar',
],
[
'type' => 'Powdered Sugar',
],
[
'type' => 'Chocolate with Sprinkles',
],
[
'type' => 'Chocolate',
],
[
'type' => 'Maple',
'id' => '0002',
'name' => 'Raised',
],
[
'type' => 'None',
],
[
'type' => 'Glazed',
],
[
'type' => 'Sugar',
],
[
'type' => 'Chocolate',
],
[
'type' => 'Maple',
'id' => '0003',
'name' => 'Old Fashioned',
],
[
'type' => 'None',
],
[
'type' => 'Glazed',
],
[
'type' => 'Chocolate',
],
[
'type' => 'Maple',
],
],
'/-/batters/batter/-,/-/name' => [
'name' => 'Old Fashioned',
[
"id" => "1001",
"type" => "Regular",
'name' => 'Cake',
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
[
'id' => '1003',
'type' => 'Blueberry',
],
[
'id' => '1004',
'type' => 'Devil\'s Food',
],
],
[
"id" => "1002",
"type" => "Chocolate",
'name' => 'Raised',
[
'id' => '1001',
'type' => 'Regular',
],
],
[
"id" => "1003",
"type" => "Blueberry",
],
[
"id" => "1004",
"type" => "Devil's Food",
'name' => 'Old Fashioned',
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
],
],
],
'complex_object' => [
'/-1,/-2' => [],
'/id,/batters/batter/-/type' => [
'id' => '0001',
'type' => "Devil's Food",
[
'id' => '0001',
'type' => 'Regular',
],
[
'type' => 'Chocolate',
],
[
'type' => 'Blueberry',
],
[
'type' => 'Devil\'s Food',
],
],
'/name,/topping/-/type,/id' => [
'id' => '0001',
'name' => 'Cake',
'type' => 'Maple',
[
'id' => '0001',
'name' => 'Cake',
'type' => 'None',
],
[
'type' => 'Glazed',
],
[
'type' => 'Sugar',
],
[
'type' => 'Powdered Sugar',
],
[
'type' => 'Chocolate with Sprinkles',
],
[
'type' => 'Chocolate',
],
[
'type' => 'Maple',
],
],
'/batters/batter/-,/type' => [
'type' => 'donut',

View File

@ -4,52 +4,166 @@ return [
'complex_array' => [
'' => $complexArray = require __DIR__ . '/../parsing/complex_array.php',
'/-' => $complexArray,
'/-/id' => ['id' => '0003'],
'/-/id' => [
[
'id' => '0001',
],
[
'id' => '0002',
],
[
'id' => '0003',
],
],
'/-/batters' => [
'batters' => [
'batter' => [
[
"id" => "1001",
"type" => "Regular",
[
'batters' => [
'batter' => [
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
[
'id' => '1003',
'type' => 'Blueberry',
],
[
'id' => '1004',
'type' => 'Devil\'s Food',
],
],
[
"id" => "1002",
"type" => "Chocolate",
],
],
[
'batters' => [
'batter' => [
[
'id' => '1001',
'type' => 'Regular',
],
],
],
],
[
'batters' => [
'batter' => [
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
],
],
],
],
'/-/batters/batter' => [
'batter' => [
[
"id" => "1001",
"type" => "Regular",
[
'batter' => [
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
[
'id' => '1003',
'type' => 'Blueberry',
],
[
'id' => '1004',
'type' => 'Devil\'s Food',
],
],
[
"id" => "1002",
"type" => "Chocolate",
],
[
'batter' => [
[
'id' => '1001',
'type' => 'Regular',
],
],
],
[
'batter' => [
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
],
],
],
'/-/batters/batter/-' => [
[
"id" => "1001",
"type" => "Regular",
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
[
'id' => '1003',
'type' => 'Blueberry',
],
[
'id' => '1004',
'type' => 'Devil\'s Food',
],
],
[
"id" => "1002",
"type" => "Chocolate",
[
'id' => '1001',
'type' => 'Regular',
],
],
[
"id" => "1003",
"type" => "Blueberry",
],
[
"id" => "1004",
"type" => "Devil's Food",
[
'id' => '1001',
'type' => 'Regular',
],
[
'id' => '1002',
'type' => 'Chocolate',
],
],
],
'/-/batters/batter/-/id' => [
[
'id' => '1001',
],
[
'id' => '1002',
],
[
'id' => '1003',
],
[
'id' => '1004',
],
[
'id' => '1001',
],
[
'id' => '1001',
],
[
'id' => '1002',
],
],
'/-/batters/batter/-/id' => ['id' => "1002"],
],
'complex_object' => [
'' => require __DIR__ . '/../parsing/complex_object.php',
@ -115,7 +229,20 @@ return [
"type" => "Devil's Food",
],
],
'/batters/batter/-/id' => ['id' => "1004"],
'/batters/batter/-/id' => [
[
'id' => '1001',
],
[
'id' => '1002',
],
[
'id' => '1003',
],
[
'id' => '1004',
],
],
],
'empty_array' => [
'' => [],