diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ceeb344..af174ad 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: cerbero90 - +ko_fi: cerbero90 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eda588e..3efe43b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,8 @@ name: build on: - push: - pull_request: + push: + pull_request: jobs: tests: @@ -11,20 +11,11 @@ jobs: strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0] - laravel: [6.*, 7.*, 8.*] + php: [8.1, 8.2, 8.3] dependency-version: [prefer-lowest, prefer-stable] - os: [ubuntu-latest, windows-latest] - exclude: - - laravel: 6.* - php: 8.0 - dependency-version: prefer-lowest - - laravel: 8.* - php: 7.2 - - os: windows-latest - php: 8.0 - - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} + os: [ubuntu-latest] + + name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} steps: - name: Checkout code @@ -34,17 +25,15 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip tools: composer:v2 coverage: none - name: Install dependencies run: | - composer require "illuminate/contracts=${{ matrix.laravel }}" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction - name: Execute tests - run: vendor/bin/phpunit --verbose + run: vendor/bin/pest coverage: runs-on: ubuntu-latest @@ -54,12 +43,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 7.4 - extensions: dom, curl, libxml, mbstring, zip + php-version: 8.1 tools: composer:v2 coverage: xdebug @@ -67,17 +57,16 @@ jobs: run: composer update --prefer-stable --prefer-dist --no-interaction - name: Execute tests - run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + run: vendor/bin/pest --coverage-text --coverage-clover=coverage.clover - name: Upload coverage run: | - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover + vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover - style: + linting: runs-on: ubuntu-latest - name: Coding style + name: Linting steps: - name: Checkout code @@ -86,9 +75,13 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 - tools: phpcs + php-version: 8.2 + tools: composer:v2 coverage: none - - name: Execute check - run: phpcs --standard=psr12 src/ tests/ + - name: Install dependencies + run: | + composer update --prefer-stable --prefer-dist --no-interaction + + - name: Execute Duster + run: vendor/bin/duster lint -u tlint,phpcodesniffer,pint,phpstan -vvv diff --git a/.gitignore b/.gitignore index 7ae6add..bd2b70e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor phpcs.xml phpunit.xml .phpunit.result.cache +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index faba229..203b4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,22 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Nothing +## 2.0.0 - 2023-11-29 + +### Added +- Recursive lazy collections for JSON objects and arrays +- Auto-registering macro for lazy collections +- Dependency from [๐Ÿงฉ JSON Parser](https://github.com/cerbero90/json-parser) +- Namespaced helper +- Compatibility with latest versions of PHP +- Pest testing framework +- Tools for static analysis + +### Removed +- Dependency from [JSON Machine](https://github.com/halaxa/json-machine) +- Compatibility with older versions of PHP + + ## 1.1.0 - 2021-05-06 ### Added diff --git a/README.md b/README.md index 5e676f7..57d0534 100644 --- a/README.md +++ b/README.md @@ -2,147 +2,183 @@ [![Author][ico-author]][link-author] [![PHP Version][ico-php]][link-php] -[![Laravel Version][ico-laravel]][link-laravel] -[![Octane Compatibility][ico-octane]][link-octane] [![Build Status][ico-actions]][link-actions] [![Coverage Status][ico-scrutinizer]][link-scrutinizer] [![Quality Score][ico-code-quality]][link-code-quality] +[![PHPStan Level][ico-phpstan]][link-phpstan] [![Latest Version][ico-version]][link-packagist] [![Software License][ico-license]](LICENSE.md) -[![PSR-7][ico-psr7]][link-psr7] -[![PSR-12][ico-psr12]][link-psr12] +[![PER][ico-per]][link-per] [![Total Downloads][ico-downloads]][link-downloads] -Framework agnostic package to load heavy JSON in [lazy collections](https://laravel.com/docs/collections#lazy-collections). Under the hood, the brilliant [JSON Machine](https://github.com/halaxa/json-machine) by [@halaxa](https://github.com/halaxa) is used as a lexer and parser. +```php +LazyCollection::fromJson($source, 'data.*.users.*') + ->map($this->mapToUser(...)) + ->filter($this->filterUser(...)) + ->values() + ->chunk(1_000) + ->each($this->storeUsersChunk(...)); +``` + +Framework-agnostic package to load JSON of any size and from any source into [Laravel lazy collections](https://laravel.com/docs/collections#lazy-collections). + +Lazy JSON recursively turns any JSON array or object into a lazy collection, consuming only a few KB of memory while parsing JSON of any dimension. -Need to load paginated items of JSON APIs? Consider using [Lazy JSON Pages](https://github.com/cerbero90/lazy-json-pages) instead. +It optionally allows to extract only some sub-trees, instead of the whole JSON, with an easy dot-notation syntax. +Under the hood, [๐Ÿงฉ JSON Parser](https://github.com/cerbero90/json-parser) is used to parse JSONs and extract sub-trees. -## Install +Need to lazy load items from paginated JSON APIs? Consider using [๐Ÿผ Lazy JSON Pages](https://github.com/cerbero90/lazy-json-pages) instead. -In a Laravel application, all you need to do is requiring the package: + +## ๐Ÿ“ฆ Install + +Via Composer: ``` bash composer require cerbero/lazy-json ``` -Otherwise, you also need to register the lazy collection macro manually: +## ๐Ÿ”ฎ Usage -```php -use Cerbero\LazyJson\Macro; -use Illuminate\Support\LazyCollection; +* [๐Ÿ‘ฃ Basics](#-basics) +* [๐Ÿ’ง Sources](#-sources) +* [๐ŸŽฏ Dots](#-dots) -LazyCollection::macro('fromJson', new Macro()); -``` -## Usage +### ๐Ÿ‘ฃ Basics -Loading JSON in lazy collections is possible by using the collection itself or the included helper: +Depending on our coding style, we can call Lazy JSON in 3 different ways: ```php -LazyCollection::fromJson($source); +use Cerbero\LazyJson\LazyJson; +use Illuminate\Support\LazyCollection; + +use function Cerbero\LazyJson\lazyJson; + +// auto-registered lazy collection macro +$lazyCollection = LazyCollection::fromJson($source); + +// static method +$lazyCollection = LazyJson::from($source); -lazyJson($source); +// namespaced helper +$lazyCollection = lazyJson($source); ``` -The following are the supported JSON sources: +The variable `$source` in our examples represents any [JSON source](#-sources). Once we define the source, we can chain any method of the [Laravel lazy collection](https://laravel.com/docs/collections#lazy-collections) to process the JSON in a memory-efficient way: ```php -$source = '{"foo":"bar"}'; // JSON string -$source = ['{"foo":"bar"}']; // any iterable containing JSON, i.e. array or Traversable -$source = 'https://foo.test/endpoint'; // endpoint -$source = Http::get('https://foo.test/endpoint'); // Laravel HTTP client response -$source = '/path/to/file.json'; // JSON file -$source = fopen('/path/to/file.json', 'rb'); // any resource -$source = ; // any PSR-7 message, e.g. Guzzle response -$source = ; // any PSR-7 stream +LazyCollection::fromJson($source) + ->values() + ->map(/* ... */) + ->where(/* ... */) + ->each(/* ... */); ``` -Optionally, you can define a dot-noted path to extract only a sub-tree of the JSON. For example, given the following JSON: - -```json -{ - "data": [ - { - "name": "Team 1", - "users": [ - { - "id": 1 - }, - { - "id": 2 - } - ] - }, - { - "name": "Team 2", - "users": [ - { - "id": 3 - } - ] - } - ] -} + +### ๐Ÿ’ง Sources + +A JSON source is any data point that provides a JSON. A wide range of sources are supported by default: +- **strings**, e.g. `{"foo":"bar"}` +- **iterables**, i.e. arrays or instances of `Traversable` +- **file paths**, e.g. `/path/to/large.json` +- **resources**, e.g. streams +- **API endpoint URLs**, e.g. `https://endpoint.json` or any instance of `Psr\Http\Message\UriInterface` +- **PSR-7 requests**, i.e. any instance of `Psr\Http\Message\RequestInterface` +- **PSR-7 messages**, i.e. any instance of `Psr\Http\Message\MessageInterface` +- **PSR-7 streams**, i.e. any instance of `Psr\Http\Message\StreamInterface` +- **Laravel HTTP client requests**, i.e. any instance of `Illuminate\Http\Client\Request` +- **Laravel HTTP client responses**, i.e. any instance of `Illuminate\Http\Client\Response` +- **user-defined sources**, i.e. any instance of `Cerbero\JsonParser\Sources\Source` + +For more information about JSON sources, please consult the [๐Ÿงฉ JSON Parser documentation](https://github.com/cerbero90/json-parser). + + +### ๐ŸŽฏ Dots + +If we only need a sub-tree of a large JSON, we can use a simple dot-notation syntax to extract the desired path (or **dot**). + +Consider [this JSON](https://randomuser.me/api/1.4?seed=json-parser&results=5) for example. To extract only the cities and avoid parsing the rest of the JSON, we can set the `results.*.location.city` dot: + +```php +$source = 'https://randomuser.me/api/1.4?seed=json-parser&results=5'; + +$dot = 'results.*.location.city'; + +LazyCollection::fromJson($source, $dot)->each(function (string $value, string $key) { + // 1st iteration: $key === 'city', $value === 'Sontra' + // 2nd iteration: $key === 'city', $value === 'San Rafael Tlanalapan' + // 3rd iteration: $key === 'city', $value === 'ฺฏุฑฺฏุงู†' + // ... +}); ``` -defining the path `data.*.users.*.id` would iterate only user IDs: +The dot-notation syntax is very simple and it can include any of the following 4 elements: +- a key of a JSON array, e.g. `0` +- a key of a JSON object, e.g. `results` +- a dot to indicate the nesting level within a JSON, e.g. `results.0` +- an asterisk to indicate all items within an array, e.g. `results.*` + +If we need to extract several sub-trees, Lazy JSON supports multiple dots: ```php -$ids = lazyJson($source, 'data.*.users.*.id') - ->filter(fn ($id) => $id % 2 == 0) - ->all(); +$dots = ['results.*.gender', 'results.*.email']; + +LazyCollection::fromJson($source, $dots)->each(function (string $value, string $key) { + // 1st iteration: $key === 'gender', $value === 'female' + // 2nd iteration: $key === 'email', $value === 'sara.meder@example.com' + // 3rd iteration: $key === 'gender', $value === 'female' + // 4th iteration: $key === 'email', $value === 'andrea.roque@example.com' + // ... +}); ``` -## Change log +## ๐Ÿ“† Change log Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. -## Testing +## ๐Ÿงช Testing ``` bash composer test ``` -## Contributing +## ๐Ÿ’ž Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. -## Security +## ๐Ÿงฏ Security If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker. -## Credits +## ๐Ÿ… Credits - [Andrea Marco Sartori][link-author] - [All Contributors][link-contributors] -## License +## โš–๏ธ License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. [ico-author]: https://img.shields.io/static/v1?label=author&message=cerbero90&color=50ABF1&logo=twitter&style=flat-square [ico-php]: https://img.shields.io/packagist/php-v/cerbero/lazy-json?color=%234F5B93&logo=php&style=flat-square -[ico-laravel]: https://img.shields.io/static/v1?label=laravel&message=%E2%89%A56.0&color=ff2d20&logo=laravel&style=flat-square -[ico-octane]: https://img.shields.io/static/v1?label=octane&message=compatible&color=ff2d20&logo=laravel&style=flat-square [ico-version]: https://img.shields.io/packagist/v/cerbero/lazy-json.svg?label=version&style=flat-square -[ico-actions]: https://img.shields.io/github/workflow/status/cerbero90/lazy-json/build?style=flat-square&logo=github +[ico-actions]: https://img.shields.io/github/actions/workflow/status/cerbero90/json-parser/build.yml?branch=master&style=flat-square&logo=github [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square -[ico-psr7]: https://img.shields.io/static/v1?label=compliance&message=PSR-7&color=blue&style=flat-square -[ico-psr12]: https://img.shields.io/static/v1?label=compliance&message=PSR-12&color=blue&style=flat-square +[ico-per]: https://img.shields.io/static/v1?label=compliance&message=PER&color=blue&style=flat-square [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/cerbero90/lazy-json.svg?style=flat-square&logo=scrutinizer [ico-code-quality]: https://img.shields.io/scrutinizer/g/cerbero90/lazy-json.svg?style=flat-square&logo=scrutinizer +[ico-phpstan]: https://img.shields.io/badge/level-max-success?style=flat-square&logo=data:image/png;base64,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 [ico-downloads]: https://img.shields.io/packagist/dt/cerbero/lazy-json.svg?style=flat-square [link-author]: https://twitter.com/cerbero90 [link-php]: https://www.php.net -[link-laravel]: https://laravel.com -[link-octane]: https://github.com/laravel/octane [link-packagist]: https://packagist.org/packages/cerbero/lazy-json [link-actions]: https://github.com/cerbero90/lazy-json/actions?query=workflow%3Abuild -[link-psr7]: https://www.php-fig.org/psr/psr-7/ -[link-psr12]: https://www.php-fig.org/psr/psr-12/ +[link-per]: https://www.php-fig.org/per/coding-style/ [link-scrutinizer]: https://scrutinizer-ci.com/g/cerbero90/lazy-json/code-structure [link-code-quality]: https://scrutinizer-ci.com/g/cerbero90/lazy-json [link-downloads]: https://packagist.org/packages/cerbero/lazy-json +[link-phpstan]: https://phpstan.org/ [link-contributors]: ../../contributors diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..3893e29 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,8 @@ +=6.0" - }, - "suggest": { - "guzzlehttp/guzzle": "Required to load JSON from endpoints (^7.0)." + "php": "^8.1", + "cerbero/json-parser": "^1.1", + "illuminate/collections": ">=8.12" }, "require-dev": { - "guzzlehttp/guzzle": "^7.0", - "mockery/mockery": "^1.3.4", - "orchestra/testbench": ">=3.9", - "phpunit/phpunit": ">=8.0", - "squizlabs/php_codesniffer": "^3.0" + "pestphp/pest": "^2.16", + "phpstan/phpstan": "^1.9", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.0", + "tightenco/duster": "^2.0" }, "autoload": { "psr-4": { "Cerbero\\LazyJson\\": "src" }, "files": [ + "bootstrap.php", "helpers.php" ] }, @@ -47,21 +44,19 @@ } }, "scripts": { - "test": "phpunit", - "check-style": "phpcs --standard=PSR12 src tests", - "fix-style": "phpcbf --standard=PSR12 src tests" + "fix": "duster fix -u tlint,phpcodesniffer,pint", + "lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan", + "test": "pest" }, "extra": { "branch-alias": { "dev-master": "1.0-dev" - }, - "laravel": { - "providers": [ - "Cerbero\\LazyJson\\Providers\\LazyJsonServiceProvider" - ] } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } } } diff --git a/duster.json b/duster.json new file mode 100644 index 0000000..e3834c7 --- /dev/null +++ b/duster.json @@ -0,0 +1,13 @@ +{ + "include": [ + "src" + ], + "exclude": [ + "tests" + ], + "scripts": { + "lint": { + "phpstan": ["./vendor/bin/phpstan", "analyse"] + } + } +} diff --git a/helpers.php b/helpers.php index b8da450..ae3b505 100644 --- a/helpers.php +++ b/helpers.php @@ -1,17 +1,13 @@ $dot + */ +function lazyJson(mixed $source, string|array $dot = ''): LazyCollection +{ + return LazyJson::from($source, $dot); } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1 @@ + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5209c3e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - src +includes: + - phpstan-baseline.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 917e093..990c7b1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,28 +1,23 @@ - - - - tests - - - - - src/ - - - - - - - - + + + + + + + + + + + tests + + + + + + + + src/ + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..03e092d --- /dev/null +++ b/pint.json @@ -0,0 +1,19 @@ +{ + "preset": "per", + "rules": { + "align_multiline_comment": true, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, + "concat_space": {"spacing": "one"}, + "explicit_string_variable": true, + "ordered_imports": { + "sort_algorithm": "alpha", + "imports_order": [ + "class", + "function", + "const" + ] + }, + "simple_to_complex_string_variable": true + } +} diff --git a/src/Concerns/EndpointAware.php b/src/Concerns/EndpointAware.php deleted file mode 100644 index 69c8972..0000000 --- a/src/Concerns/EndpointAware.php +++ /dev/null @@ -1,25 +0,0 @@ -getMessage()); + } } diff --git a/src/Handlers/Endpoint.php b/src/Handlers/Endpoint.php deleted file mode 100644 index 1da7a11..0000000 --- a/src/Handlers/Endpoint.php +++ /dev/null @@ -1,61 +0,0 @@ -isEndpoint($source); - } - - /** - * Handle the given source - * - * @param mixed $source - * @param string $path - * @return Traversable - */ - public function handle($source, string $path): Traversable - { - if (!$this->guzzleIsLoaded()) { - throw new LazyJsonException('Guzzle is required to load JSON from endpoints'); - } - - $response = (new Client())->get($source, [ - 'headers' => [ - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ], - ]); - - return parent::handle($response, $path); - } - - /** - * Determine whether Guzzle is loaded, useful for testing - * - * @return bool - */ - protected function guzzleIsLoaded(): bool - { - return class_exists(Client::class); - } -} diff --git a/src/Handlers/Filename.php b/src/Handlers/Filename.php deleted file mode 100644 index 811caee..0000000 --- a/src/Handlers/Filename.php +++ /dev/null @@ -1,39 +0,0 @@ -toJsonPointer($path)); - } -} diff --git a/src/Handlers/Handler.php b/src/Handlers/Handler.php deleted file mode 100644 index 59a9966..0000000 --- a/src/Handlers/Handler.php +++ /dev/null @@ -1,29 +0,0 @@ -toJsonPointer($path)); - } -} diff --git a/src/Handlers/JsonString.php b/src/Handlers/JsonString.php deleted file mode 100644 index 9e7e505..0000000 --- a/src/Handlers/JsonString.php +++ /dev/null @@ -1,41 +0,0 @@ -isEndpoint($source); - } - - /** - * Handle the given source - * - * @param mixed $source - * @param string $path - * @return Traversable - */ - public function handle($source, string $path): Traversable - { - return JsonMachine::fromString($source, $this->toJsonPointer($path)); - } -} diff --git a/src/Handlers/LaravelClientResponse.php b/src/Handlers/LaravelClientResponse.php deleted file mode 100644 index 34fa6dd..0000000 --- a/src/Handlers/LaravelClientResponse.php +++ /dev/null @@ -1,36 +0,0 @@ -toPsrResponse(), $path); - } -} diff --git a/src/Handlers/Psr7Message.php b/src/Handlers/Psr7Message.php deleted file mode 100644 index 8d67a28..0000000 --- a/src/Handlers/Psr7Message.php +++ /dev/null @@ -1,36 +0,0 @@ -getBody(), $path); - } -} diff --git a/src/Handlers/Psr7Stream.php b/src/Handlers/Psr7Stream.php deleted file mode 100644 index 7ce57ea..0000000 --- a/src/Handlers/Psr7Stream.php +++ /dev/null @@ -1,45 +0,0 @@ - ['stream' => $source], - ])); - - return parent::handle($stream, $path); - } -} diff --git a/src/Handlers/Resource.php b/src/Handlers/Resource.php deleted file mode 100644 index f82eee4..0000000 --- a/src/Handlers/Resource.php +++ /dev/null @@ -1,39 +0,0 @@ -toJsonPointer($path)); - } -} diff --git a/src/LazyJson.php b/src/LazyJson.php new file mode 100644 index 0000000..99b62e0 --- /dev/null +++ b/src/LazyJson.php @@ -0,0 +1,53 @@ + + */ +final class LazyJson implements IteratorAggregate +{ + private JsonParser $parser; + + /** + * @param string|string[]|array $dot + * @return LazyCollection + */ + public static function from(mixed $source, string|array $dot = '*'): LazyCollection + { + return new LazyCollection(fn() => yield from new self($source, (array) $dot)); + } + + /** + * @param string[]|array $dots + */ + private function __construct(mixed $source, array $dots) + { + $this->parser = JsonParser::parse($source) + ->lazyPointers(DotsConverter::toPointers($dots)) + ->wrap(fn(Parser $parser) => new LazyCollection(fn() => yield from $parser)); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + try { + yield from $this->parser; + } catch (Throwable $e) { + throw new LazyJsonException($e); + } + } +} diff --git a/src/Macro.php b/src/Macro.php deleted file mode 100644 index 0470b3a..0000000 --- a/src/Macro.php +++ /dev/null @@ -1,32 +0,0 @@ -getMessage(), 0, $e); - } - }); - } -} diff --git a/src/Pointers/DotsConverter.php b/src/Pointers/DotsConverter.php new file mode 100644 index 0000000..8d8f7fa --- /dev/null +++ b/src/Pointers/DotsConverter.php @@ -0,0 +1,37 @@ + $dots + * @return string[]|array + */ + public static function toPointers(array $dots): array + { + $pointers = []; + + foreach ($dots as $dot => $callback) { + if ($callback instanceof Closure) { + $pointers[self::toPointer($dot)] = $callback; + } else { + $pointers[] = self::toPointer($callback); + } + } + + return $pointers; + } + + public static function toPointer(string $dot): string + { + $search = ['~', '/', '.', '*', '\\', '"']; + $replace = ['~0', '~1', '/', '-', '\\\\', '\"']; + + return $dot == '*' ? '' : '/' . str_replace($search, $replace, $dot); + } +} diff --git a/src/Providers/LazyJsonServiceProvider.php b/src/Providers/LazyJsonServiceProvider.php deleted file mode 100644 index 5fea13e..0000000 --- a/src/Providers/LazyJsonServiceProvider.php +++ /dev/null @@ -1,24 +0,0 @@ -traversable = $this->toTraversable($source, $path); - } - - /** - * Turn the given JSON source into a traversable instance - * - * @param mixed $source - * @param string $path - * @return Traversable - * - * @throws LazyJsonException - */ - protected function toTraversable($source, string $path): Traversable - { - foreach ($this->handlers as $class) { - /** @var Handlers\Handler $handler */ - $handler = new $class(); - - if ($handler->handles($source)) { - return $handler->handle($source, $path); - } - } - - throw new LazyJsonException('Unable to load the JSON from the provided source.'); - } - - /** - * Retrieve the traversable JSON - * - * @return Traversable - */ - public function getIterator(): Traversable - { - return $this->traversable; - } -} diff --git a/src/StreamWrapper.php b/src/StreamWrapper.php deleted file mode 100644 index 0027c0f..0000000 --- a/src/StreamWrapper.php +++ /dev/null @@ -1,75 +0,0 @@ -context); - - $this->stream = $options[static::NAME]['stream'] ?? null; - - return $this->stream instanceof StreamInterface && $this->stream->isReadable(); - } - - /** - * Determine whether the pointer is at the end of the stream - * - * @return bool - */ - public function stream_eof(): bool - { - return $this->stream->eof(); - } - - /** - * Read from the stream - * - * @param int $count - * @return string - */ - public function stream_read(int $count): string - { - return $this->stream->read($count); - } -} diff --git a/tests/Dataset.php b/tests/Dataset.php new file mode 100644 index 0000000..423ff88 --- /dev/null +++ b/tests/Dataset.php @@ -0,0 +1,46 @@ + $value) { + yield [$source, $key, $value]; + } + } + + public static function forSingleDots(): Generator + { + $singleDot = require fixture('single_dot.php'); + + foreach ($singleDot as $fixture => $subtreeByDot) { + $source = fixture("{$fixture}.json"); + + foreach ($subtreeByDot as $dot => $expectedValuesByKey) { + yield [$source, $dot, $expectedValuesByKey]; + } + } + } + + public static function forMultipleDots(): Generator + { + $singleDot = require fixture('multiple_dots.php'); + + foreach ($singleDot as $fixture => $subtreeByDots) { + $source = fixture("{$fixture}.json"); + + foreach ($subtreeByDots as $dots => $expectedValuesByKey) { + yield [$source, explode(',', $dots), $expectedValuesByKey]; + } + } + } +} diff --git a/tests/Feature/LazyJsonTest.php b/tests/Feature/LazyJsonTest.php new file mode 100644 index 0000000..28f0c33 --- /dev/null +++ b/tests/Feature/LazyJsonTest.php @@ -0,0 +1,89 @@ +toBeInstanceOf(LazyCollection::class); +}); + +it('can be used via static method', function () { + expect(LazyJson::from('{"foo":123}'))->toBeInstanceOf(LazyCollection::class); +}); + +it('can be used via namespaced helper', function () { + expect(lazyJson('{"foo":123}'))->toBeInstanceOf(LazyCollection::class); +}); + +it('wraps the thrown exception when an error occurs', function () { + expect(fn () => LazyJson::from('/foo')->all())->toThrow(fn (LazyJsonException $e) => expect($e) + ->getMessage()->toBe("Syntax error: unexpected '/' at position 0") + ->exception->toBeInstanceOf(SyntaxException::class) + ); +}); + +it('iterates through keys and values', function () { + expect(LazyJson::from('{"foo":123,"bar":321}'))->sequence( + fn (Expectation $value, Expectation $key) => $key->toBe('foo')->and($value)->toBe(123), + fn (Expectation $value, Expectation $key) => $key->toBe('bar')->and($value)->toBe(321), + ); +}); + +it('wraps JSON objects and arrays into lazy collections', function () { + expect(LazyJson::from('{"foo":{"one":1,"two":2},"bar":[3,4]}')) + ->sequence(fn (Expectation $value) => $value->toBeWrappedIntoLazyCollection()); +}); + +it('turns dot notation into JSON pointers correctly', function (string $source, string|int $dot, mixed $sequence) { + expect(LazyJson::from($source, (string) $dot))->sequence($sequence); +})->with(Dataset::forDots()); + +it('sets a callable JSON pointer by using the dot notation syntax', function () { + $dots = [ + 'foo' => function ($value, $key) { + expect($key)->toBe('foo')->and($value)->toBeInstanceOf(LazyCollection::class)->values()->all()->toBe([1, 2]); + return 'foo closure was run'; + }, + 'bar' => function ($value, $key) { + expect($key)->toBe('bar')->and($value)->toBeInstanceOf(LazyCollection::class)->all()->toBe([3, 4]); + return 'bar closure was run'; + }, + ]; + + expect(LazyJson::from('{"foo":{"one":1,"two":2},"bar":[3,4]}', $dots))->sequence( + fn (Expectation $value, Expectation $key) => $key->toBe('foo')->and($value)->toBe('foo closure was run'), + fn (Expectation $value, Expectation $key) => $key->toBe('bar')->and($value)->toBe('bar closure was run'), + ); +}); + +it('sets a JSON pointer by using the dot notation syntax', function (string $source, string $dot, array $expectedValuesByKey) { + $actualValues = []; + $expectedKey = key($expectedValuesByKey); + $expectedValues = reset($expectedValuesByKey); + + expect(LazyJson::from($source, $dot)) + ->sequence(function (Expectation $value, Expectation $key) use (&$actualValues, $expectedKey) { + $key->toBe($expectedKey)->and($value)->toBeInstanceOf(LazyCollection::class); + $actualValues[] = $value->value->toArray(); + }); + + expect($actualValues)->toBe($expectedValues); +})->with(Dataset::forSingleDots()); + +it('sets JSON pointers by using the dot notation syntax', function (string $source, array $dots, array $expectedValuesByKey) { + $actualValues = []; + + expect(LazyJson::from($source, $dots)) + ->sequence(function (Expectation $value, Expectation $key) use (&$actualValues) { + $value->toBeInstanceOf(LazyCollection::class); + $actualValues[$key->value][] = $value->value->toArray(); + }); + + expect($actualValues)->toBe($expectedValuesByKey); +})->with(Dataset::forMultipleDots()); diff --git a/tests/Handlers/EndpointTest.php b/tests/Handlers/EndpointTest.php deleted file mode 100644 index e973e36..0000000 --- a/tests/Handlers/EndpointTest.php +++ /dev/null @@ -1,101 +0,0 @@ -assertTrue($handler->handles('http://endpoint.test')); - $this->assertTrue($handler->handles('https://endpoint.test')); - $this->assertFalse($handler->handles(123)); - $this->assertFalse($handler->handles('http://')); - $this->assertFalse($handler->handles('https://')); - $this->assertFalse($handler->handles('ftp://foo.test')); - } - - /** - * @test - */ - public function handleEndpoint() - { - m::mock('overload:' . Client::class, [ - 'get' => new Psr7Response(200, [], '{"end":"point"}'), - ]); - - $handled = (new Endpoint())->handle('https://endpoint.test', ''); - - $this->assertInstanceOf(JsonMachine::class, $handled); - - foreach ($handled as $key => $value) { - $this->assertSame('end', $key); - $this->assertSame('point', $value); - } - } - - /** - * @test - */ - public function extractsJsonSubtrees() - { - m::mock('overload:' . Client::class, [ - 'get' => new Psr7Response(200, [], '{"foo":{"bar":1,"baz":2}}'), - ]); - - $handled = (new Endpoint())->handle('https://endpoint.test', 'foo.bar'); - - $this->assertInstanceOf(JsonMachine::class, $handled); - - foreach ($handled as $key => $value) { - $this->assertSame('bar', $key); - $this->assertSame(1, $value); - } - } - - /** - * @test - */ - public function failsIfGuzzleIsNotLoaded() - { - $double = m::mock(Endpoint::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods() - ->shouldReceive('guzzleIsLoaded') - ->once() - ->andReturn(false) - ->getMock(); - - $this->expectExceptionObject(new LazyJsonException('Guzzle is required to load JSON from endpoints')); - - $double->handle('https://endpoint.test', 'foo.bar'); - } -} diff --git a/tests/LazyJsonTest.php b/tests/LazyJsonTest.php deleted file mode 100644 index 3827869..0000000 --- a/tests/LazyJsonTest.php +++ /dev/null @@ -1,144 +0,0 @@ -each(function ($value, $key) { - $this->assertSame('key', $key); - $this->assertSame('JSON file value', $value); - }); - } - - /** - * @test - */ - public function lazyLoadsJsonFromIterable() - { - lazyJson(['{"foo":"bar"}'])->each(function ($value, $key) { - $this->assertSame('foo', $key); - $this->assertSame('bar', $value); - }); - } - - /** - * @test - */ - public function lazyLoadsJsonFromString() - { - lazyJson('{"bar":"baz"}')->each(function ($value, $key) { - $this->assertSame('bar', $key); - $this->assertSame('baz', $value); - }); - } - - /** - * @test - */ - public function lazyLoadsJsonFromLaravelClientResponse() - { - if (!class_exists(Response::class)) { - $this->markTestSkipped('The Laravel HTTP client is not loaded.'); - } - - $response = new Response(new Psr7Response(200, [], '{"status":"success"}')); - - lazyJson($response)->each(function ($value, $key) { - $this->assertSame('status', $key); - $this->assertSame('success', $value); - }); - } - - /** - * @test - */ - public function lazyLoadsJsonFromPsr7Message() - { - $response = new Psr7Response(200, [], '{"one":"two"}'); - - lazyJson($response)->each(function ($value, $key) { - $this->assertSame('one', $key); - $this->assertSame('two', $value); - }); - } - - /** - * @test - */ - public function lazyLoadsJsonFromPsr7Stream() - { - $stream = new Stream(fopen(__DIR__ . '/stub.json', 'rb')); - - lazyJson($stream)->each(function ($value, $key) { - $this->assertSame('key', $key); - $this->assertSame('JSON file value', $value); - }); - } - - /** - * @test - */ - public function lazyLoadsJsonFromResource() - { - $resource = fopen(__DIR__ . '/stub.json', 'rb'); - - lazyJson($resource)->each(function ($value, $key) { - $this->assertSame('key', $key); - $this->assertSame('JSON file value', $value); - }); - } - - /** - * @test - */ - public function failsWithInvalidJsonSource() - { - $this->expectExceptionObject(new LazyJsonException('Unable to load the JSON from the provided source.')); - - lazyJson(123)->all(); - } - - /** - * @test - */ - public function trowsPackageExceptionWhenAnyExceptionOccursDuringJsonLoading() - { - try { - lazyJson('{}}')->all(); - } catch (Throwable $e) { - $this->assertInstanceOf(LazyJsonException::class, $e); - $this->assertSame($e->getPrevious()->getMessage(), $e->getMessage()); - } - } -} diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..7443a5d --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,23 @@ +extend('toBeWrappedIntoLazyCollection', function () { + return $this->when(is_object($this->value), fn (Expectation $value) => $value + ->toBeInstanceOf(LazyCollection::class) + ->not->toBeInstanceOf(Parser::class) + ->sequence(fn (Expectation $value) => $value->toBeWrappedIntoLazyCollection()) + ); +}); diff --git a/tests/StreamWrapperTest.php b/tests/StreamWrapperTest.php deleted file mode 100644 index 51f3b51..0000000 --- a/tests/StreamWrapperTest.php +++ /dev/null @@ -1,126 +0,0 @@ - true, - ]); - - $resource = $this->openStreamWith($double); - - $this->assertTrue(is_resource($resource)); - } - - /** - * Open the stream with the given wrapper - * - * @param mixed $stream - * @return resource|bool - */ - protected function openStreamWith($stream) - { - return @fopen(StreamWrapper::NAME . '://stream', 'rb', false, stream_context_create([ - StreamWrapper::NAME => compact('stream'), - ])); - } - - /** - * @test - */ - public function cannotOpenInvalidStream() - { - $bool = $this->openStreamWith(new \stdClass()); - - $this->assertFalse($bool); - } - - /** - * @test - */ - public function cannotOpenUnreadableStream() - { - $double = m::mock(StreamInterface::class, [ - 'isReadable' => false, - ]); - - $bool = $this->openStreamWith($double); - - $this->assertFalse($bool); - } - - /** - * @test - */ - public function canReadEof() - { - $double = m::mock(StreamInterface::class, [ - 'isReadable' => true, - 'eof' => true, - ]); - - $resource = $this->openStreamWith($double); - - $this->assertTrue(is_resource($resource)); - $this->assertTrue(feof($resource)); - } - - /** - * @test - */ - public function canRead() - { - $double = m::mock(StreamInterface::class, [ - 'isReadable' => true, - 'eof' => true, - ]) - ->shouldReceive('read') - ->andReturn('abc') - ->getMock(); - - $resource = $this->openStreamWith($double); - - $this->assertTrue(is_resource($resource)); - $this->assertSame('abc', fread($resource, 3)); - } -} diff --git a/tests/fixtures/complex_array.json b/tests/fixtures/complex_array.json new file mode 100644 index 0000000..70ddac0 --- /dev/null +++ b/tests/fixtures/complex_array.json @@ -0,0 +1,70 @@ +[ + { + "id": "0001", + "type": "donut", + "name": "Cake", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" }, + { "id": "1003", "type": "Blueberry" }, + { "id": "1004", "type": "Devil's Food" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5007", "type": "Powdered Sugar" }, + { "id": "5006", "type": "Chocolate with Sprinkles" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + }, + { + "id": "0002", + "type": "donut", + "name": "Raised", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + }, + { + "id": "0003", + "type": "donut", + "name": "Old Fashioned", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + } +] diff --git a/tests/fixtures/complex_object.json b/tests/fixtures/complex_object.json new file mode 100644 index 0000000..ef2b654 --- /dev/null +++ b/tests/fixtures/complex_object.json @@ -0,0 +1,26 @@ +{ + "id": "0001", + "type": "donut", + "name": "Cake", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" }, + { "id": "1003", "type": "Blueberry" }, + { "id": "1004", "type": "Devil's Food" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5007", "type": "Powdered Sugar" }, + { "id": "5006", "type": "Chocolate with Sprinkles" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] +} diff --git a/tests/fixtures/multiple_dots.php b/tests/fixtures/multiple_dots.php new file mode 100644 index 0000000..e17e46e --- /dev/null +++ b/tests/fixtures/multiple_dots.php @@ -0,0 +1,172 @@ + [ + '*.batters.batter,*.topping' => [ + 'batter' => [ + [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + [ + "id" => "1003", + "type" => "Blueberry", + ], + [ + "id" => "1004", + "type" => "Devil's Food", + ], + ], + [ + [ + "id" => "1001", + "type" => "Regular", + ], + ], + [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + ], + ], + 'topping' => [ + [ + [ + "id" => "5001", + "type" => "None", + ], + [ + "id" => "5002", + "type" => "Glazed", + ], + [ + "id" => "5005", + "type" => "Sugar", + ], + [ + "id" => "5007", + "type" => "Powdered Sugar", + ], + [ + "id" => "5006", + "type" => "Chocolate with Sprinkles", + ], + [ + "id" => "5003", + "type" => "Chocolate", + ], + [ + "id" => "5004", + "type" => "Maple", + ], + ], + [ + [ + "id" => "5001", + "type" => "None", + ], + [ + "id" => "5002", + "type" => "Glazed", + ], + [ + "id" => "5005", + "type" => "Sugar", + ], + [ + "id" => "5003", + "type" => "Chocolate", + ], + [ + "id" => "5004", + "type" => "Maple", + ], + ], + [ + [ + "id" => "5001", + "type" => "None", + ], + [ + "id" => "5002", + "type" => "Glazed", + ], + [ + "id" => "5003", + "type" => "Chocolate", + ], + [ + "id" => "5004", + "type" => "Maple", + ], + ], + ], + ], + ], + 'complex_object' => [ + 'batters.batter,topping' => [ + 'batter' => [ + [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + [ + "id" => "1003", + "type" => "Blueberry", + ], + [ + "id" => "1004", + "type" => "Devil's Food", + ], + ], + ], + 'topping' => [ + [ + [ + "id" => "5001", + "type" => "None", + ], + [ + "id" => "5002", + "type" => "Glazed", + ], + [ + "id" => "5005", + "type" => "Sugar", + ], + [ + "id" => "5007", + "type" => "Powdered Sugar", + ], + [ + "id" => "5006", + "type" => "Chocolate with Sprinkles", + ], + [ + "id" => "5003", + "type" => "Chocolate", + ], + [ + "id" => "5004", + "type" => "Maple", + ], + ], + ], + ], + ], +]; diff --git a/tests/fixtures/simple_object.json b/tests/fixtures/simple_object.json new file mode 100644 index 0000000..137929c --- /dev/null +++ b/tests/fixtures/simple_object.json @@ -0,0 +1,22 @@ +{ + "int": 1, + "empty_string": "", + "string": "foo", + "escaped_string": "\"bar\"", + "\"escaped_key\"": "baz", + "unicode": "hej dรฅ", + "float": 3.14, + "bool": false, + "null": null, + "empty_array": [], + "empty_object": {}, + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 +} diff --git a/tests/fixtures/simple_object.php b/tests/fixtures/simple_object.php new file mode 100644 index 0000000..1bcaadb --- /dev/null +++ b/tests/fixtures/simple_object.php @@ -0,0 +1,26 @@ + 1, + 'empty_string' => '', + 'string' => 'foo', + 'escaped_string' => '"bar"', + '"escaped_key"' => 'baz', + "unicode" => "hej dรฅ", + 'float' => 3.14, + 'bool' => false, + 'null' => null, + 'empty_array' => new LazyCollection(function () {}), + 'empty_object' => new LazyCollection(function () {}), + '' => 0, + 'a/b' => 1, + 'c%d' => 2, + 'e^f' => 3, + 'g|h' => 4, + 'i\\j' => 5, + 'k"l' => 6, + ' ' => 7, + 'm~n' => 8 +]; diff --git a/tests/fixtures/single_dot.php b/tests/fixtures/single_dot.php new file mode 100644 index 0000000..a645fdc --- /dev/null +++ b/tests/fixtures/single_dot.php @@ -0,0 +1,136 @@ + [ + '*.batters' => [ + 'batters' => [ + [ + 'batter' => [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + [ + "id" => "1003", + "type" => "Blueberry", + ], + [ + "id" => "1004", + "type" => "Devil's Food", + ], + ], + ], + [ + 'batter' => [ + [ + "id" => "1001", + "type" => "Regular", + ], + ], + ], + [ + 'batter' => [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + ], + ], + ], + ], + '*.batters.batter' => [ + 'batter' => [ + [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + [ + "id" => "1003", + "type" => "Blueberry", + ], + [ + "id" => "1004", + "type" => "Devil's Food", + ], + ], + [ + [ + "id" => "1001", + "type" => "Regular", + ], + ], + [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + ], + ], + ], + ], + 'complex_object' => [ + 'batters' => [ + 'batters' => [ + [ + 'batter' => [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + [ + "id" => "1003", + "type" => "Blueberry", + ], + [ + "id" => "1004", + "type" => "Devil's Food", + ], + ], + ], + ], + ], + 'batters.batter' => [ + 'batter' => [ + [ + [ + "id" => "1001", + "type" => "Regular", + ], + [ + "id" => "1002", + "type" => "Chocolate", + ], + [ + "id" => "1003", + "type" => "Blueberry", + ], + [ + "id" => "1004", + "type" => "Devil's Food", + ], + ], + ], + ], + ], +]; diff --git a/tests/stub.json b/tests/stub.json deleted file mode 100644 index 7c13e8a..0000000 --- a/tests/stub.json +++ /dev/null @@ -1 +0,0 @@ -{"key":"JSON file value"}