Skip to content

Commit fa28a52

Browse files
committed
Merge branch 'release/1.1.0'
2 parents bf7dba4 + f3898b8 commit fa28a52

File tree

11 files changed

+424
-61
lines changed

11 files changed

+424
-61
lines changed

.gitignore

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
build
2-
composer.lock
2+
profiling
33
vendor
4-
phpcs.xml
5-
phpunit.xml
4+
.DS_Store
65
.phpunit.cache
76
.phpunit.result.cache
7+
composer.lock
8+
phpcs.xml
9+
phpunit.xml

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
2222
- Nothing
2323

2424

25+
## 1.1.0 - 2023-08-06
26+
27+
### Added
28+
- Ability to wrap Parser instances recursively when lazy loading
29+
- Support for turning Parser wrappers into array
30+
- Support for turning sub-trees using wildcards into array
31+
32+
2533
## 1.0.0 - 2023-06-16
2634

2735
### Added

README.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ $array = JsonParser::parse($source)->pointers(['/results/0/gender', '/results/0/
270270

271271
### 🐼 Lazy pointers
272272

273-
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.
273+
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.
274274

275275
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.
276276

@@ -323,6 +323,25 @@ foreach (JsonParser::parse($source)->lazy() as $key => $value) {
323323
}
324324
```
325325

326+
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:
327+
328+
```php
329+
$json = JsonParser::parse($source)
330+
->wrap(fn (Parser $parser) => new MyWrapper(fn () => yield from $parser))
331+
->lazy();
332+
333+
foreach ($json as $key => $value) {
334+
// 1st iteration: $key === 'results', $value instanceof MyWrapper
335+
foreach ($value as $nestedKey => $nestedValue) {
336+
// 1st iteration: $nestedKey === 0, $nestedValue instanceof MyWrapper
337+
// 2nd iteration: $nestedKey === 1, $nestedValue instanceof MyWrapper
338+
// ...
339+
}
340+
}
341+
```
342+
343+
> ℹ️ If your wrapper class implements the method `toArray()`, such method will be called when eager loading sub-trees into an array.
344+
326345
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:
327346

328347
```php

src/JsonParser.php

+13
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,17 @@ public function onSyntaxError(Closure $callback): self
246246

247247
return $this;
248248
}
249+
250+
/**
251+
* Set the logic to run for wrapping the parser
252+
*
253+
* @param Closure $callback
254+
* @return self
255+
*/
256+
public function wrap(Closure $callback): self
257+
{
258+
$this->config->wrapper = $callback;
259+
260+
return $this;
261+
}
249262
}

src/Tokens/Parser.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ public function getIterator(): Traversable
7171
/** @var string|int $key */
7272
$key = $this->decoder->decode($state->tree->currentKey());
7373
$value = $this->decoder->decode($state->value());
74+
$wrapper = $value instanceof self ? ($this->config->wrapper)($value) : $value;
7475

75-
yield $key => $state->callPointer($value, $key);
76+
yield $key => $state->callPointer($wrapper, $key);
7677

7778
$value instanceof self && $value->fastForward();
7879
}
@@ -112,13 +113,21 @@ public function lazyLoad(): Generator
112113
*/
113114
public function toArray(): array
114115
{
116+
$index = 0;
115117
$array = [];
118+
$hasWildcards = false;
116119

117120
foreach ($this as $key => $value) {
118-
$array[$key] = $value instanceof self ? $value->toArray() : $value;
121+
if (isset($array[$index][$key])) {
122+
$index++;
123+
$hasWildcards = true;
124+
}
125+
126+
$turnsIntoArray = is_object($value) && method_exists($value, 'toArray');
127+
$array[$index][$key] = $turnsIntoArray ? $value->toArray() : $value;
119128
}
120129

121-
return $array;
130+
return $hasWildcards || empty($array) ? $array : $array[0];
122131
}
123132

124133
/**

src/ValueObjects/Config.php

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Cerbero\JsonParser\Exceptions\SyntaxException;
1111
use Cerbero\JsonParser\Pointers\Pointer;
1212
use Cerbero\JsonParser\Pointers\Pointers;
13+
use Cerbero\JsonParser\Tokens\Parser;
1314
use Closure;
1415

1516
/**
@@ -53,6 +54,13 @@ final class Config
5354
*/
5455
public Closure $onSyntaxError;
5556

57+
/**
58+
* The callback to run for wrapping the parser.
59+
*
60+
* @var Closure
61+
*/
62+
public Closure $wrapper;
63+
5664
/**
5765
* Instantiate the class
5866
*
@@ -63,6 +71,7 @@ public function __construct()
6371
$this->pointers = new Pointers();
6472
$this->onDecodingError = fn (DecodedValue $decoded) => throw new DecodingException($decoded);
6573
$this->onSyntaxError = fn (SyntaxException $e) => throw $e;
74+
$this->wrapper = fn (Parser $parser) => $parser;
6675
}
6776

6877
/**

src/ValueObjects/State.php

-10
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,6 @@ public function __construct(private readonly Pointers $pointers, private readonl
5454
$this->tree = new Tree($pointers);
5555
}
5656

57-
/**
58-
* Retrieve the JSON tree
59-
*
60-
* @return Tree
61-
*/
62-
public function tree(): Tree
63-
{
64-
return $this->tree;
65-
}
66-
6757
/**
6858
* Determine whether the parser can stop parsing
6959
*

tests/Feature/ParsingTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
use Cerbero\JsonParser\Dataset;
44
use Cerbero\JsonParser\Decoders\SimdjsonDecoder;
55
use Cerbero\JsonParser\JsonParser;
6+
use Cerbero\JsonParser\Tokens\Parser;
7+
use Pest\Expectation;
68

79
use function Cerbero\JsonParser\parseJson;
810

@@ -42,3 +44,9 @@
4244

4345
expect($parser->progress()->percentage())->toBe(100.0);
4446
});
47+
48+
it('wraps the parser recursively', function (string $source) {
49+
$json = JsonParser::parse($source)->wrap(fn (Parser $parser) => yield from $parser)->lazy();
50+
51+
expect($json)->traverse(fn (Expectation $value) => $value->toBeWrappedInto(Generator::class));
52+
})->with([fixture('json/complex_array.json'), fixture('json/complex_array.json')]);

tests/Pest.php

+54
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Cerbero\JsonParser\Tokens\Parser;
4+
use Pest\Expectation;
45

56
if (!function_exists('fixture')) {
67
/**
@@ -15,6 +16,43 @@ function fixture(string $fixture): string
1516
}
1617
}
1718

19+
/**
20+
* Expect the given sequence from a Traversable
21+
* Temporary fix to sequence() until this PR is merged: https://github.com/pestphp/pest/pull/895
22+
*
23+
* @param mixed ...$callbacks
24+
* @return Expectation
25+
*/
26+
expect()->extend('traverse', function (mixed ...$callbacks) {
27+
if (! is_iterable($this->value)) {
28+
throw new BadMethodCallException('Expectation value is not iterable.');
29+
}
30+
31+
if (empty($callbacks)) {
32+
throw new InvalidArgumentException('No sequence expectations defined.');
33+
}
34+
35+
$index = $valuesCount = 0;
36+
37+
foreach ($this->value as $key => $value) {
38+
$valuesCount++;
39+
40+
if ($callbacks[$index] instanceof Closure) {
41+
$callbacks[$index](new self($value), new self($key));
42+
} else {
43+
(new self($value))->toEqual($callbacks[$index]);
44+
}
45+
46+
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
47+
}
48+
49+
if (count($callbacks) > $valuesCount) {
50+
throw new OutOfRangeException('Sequence expectations are more than the iterable items');
51+
}
52+
53+
return $this;
54+
});
55+
1856
/**
1957
* Expect that keys and values are parsed correctly
2058
*
@@ -74,4 +112,20 @@ function fixture(string $fixture): string
74112
expect($key)->toBe($expectedKey)->and($value)->toLazyLoadRecursively($keys, $expected);
75113
}
76114
}
115+
116+
return $this;
117+
});
118+
119+
/**
120+
* Expect that all Parser instances are wrapped recursively
121+
*
122+
* @param string $wrapper
123+
* @return Expectation
124+
*/
125+
expect()->extend('toBeWrappedInto', function (string $wrapper) {
126+
return $this->when(is_object($this->value), fn (Expectation $value) => $value
127+
->toBeInstanceOf($wrapper)
128+
->not->toBeInstanceOf(Parser::class)
129+
->traverse(fn (Expectation $value) => $value->toBeWrappedInto($wrapper))
130+
);
77131
});

0 commit comments

Comments
 (0)