mirror of
https://github.com/cerbero90/json-parser.git
synced 2025-01-17 04:58:15 +01:00
Merge branch 'feature/wrapper' into develop
This commit is contained in:
commit
bf58862b96
8
.gitignore
vendored
8
.gitignore
vendored
@ -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
|
||||
|
21
README.md
21
README.md
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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')]);
|
||||
|
@ -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))
|
||||
);
|
||||
});
|
||||
|
@ -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',
|
||||
|
183
tests/fixtures/pointers/single_pointer_to_array.php
vendored
183
tests/fixtures/pointers/single_pointer_to_array.php
vendored
@ -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' => [
|
||||
'' => [],
|
||||
|
Loading…
x
Reference in New Issue
Block a user