Skip to content

Commit

Permalink
Merge branch 'release/1.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Aug 6, 2023
2 parents bf7dba4 + f3898b8 commit fa28a52
Show file tree
Hide file tree
Showing 11 changed files with 424 additions and 61 deletions.
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
- Nothing


## 1.1.0 - 2023-08-06

### Added
- Ability to wrap Parser instances recursively when lazy loading
- Support for turning Parser wrappers into array
- Support for turning sub-trees using wildcards into array


## 1.0.0 - 2023-06-16

### Added
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/JsonParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,17 @@ public function onSyntaxError(Closure $callback): self

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;
}
}
15 changes: 12 additions & 3 deletions src/Tokens/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ public function getIterator(): Traversable
/** @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();
}
Expand Down Expand Up @@ -112,13 +113,21 @@ public function lazyLoad(): Generator
*/
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];
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/ValueObjects/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Cerbero\JsonParser\Exceptions\SyntaxException;
use Cerbero\JsonParser\Pointers\Pointer;
use Cerbero\JsonParser\Pointers\Pointers;
use Cerbero\JsonParser\Tokens\Parser;
use Closure;

/**
Expand Down Expand Up @@ -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
*
Expand All @@ -63,6 +71,7 @@ public function __construct()
$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;
}

/**
Expand Down
10 changes: 0 additions & 10 deletions src/ValueObjects/State.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,6 @@ public function __construct(private readonly Pointers $pointers, private readonl
$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
*
Expand Down
8 changes: 8 additions & 0 deletions tests/Feature/ParsingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,3 +44,9 @@

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')]);
54 changes: 54 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Cerbero\JsonParser\Tokens\Parser;
use Pest\Expectation;

if (!function_exists('fixture')) {
/**
Expand All @@ -15,6 +16,43 @@ function fixture(string $fixture): string
}
}

/**
* 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
*
Expand Down Expand Up @@ -74,4 +112,20 @@ function fixture(string $fixture): string
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))
);
});

0 comments on commit fa28a52

Please sign in to comment.