From ef2ee9abcc464556b5de00fec073052cac7240fb Mon Sep 17 00:00:00 2001 From: Andrea Marco Sartori Date: Tue, 19 Dec 2023 20:03:03 +1000 Subject: [PATCH] Scaffolding second release --- .github/FUNDING.yml | 1 + .github/workflows/build.yml | 50 +-- .gitignore | 1 + .scrutinizer.yml | 2 + CHANGELOG.md | 2 +- README.md | 78 ++-- bootstrap.php | 10 + composer.json | 38 +- duster.json | 13 + helpers.php | 26 +- phpstan-baseline.neon | 1 + phpstan.neon | 6 + phpunit.xml.dist | 47 +-- pint.json | 19 + src/Concerns/HandlesTotalPages.php | 109 ----- src/Concerns/RetriesHttpRequests.php | 88 ---- src/Dtos/Config.php | 27 ++ src/Exceptions/LazyJsonPagesException.php | 12 +- src/Exceptions/OutOfAttemptsException.php | 51 --- src/Exceptions/UnsupportedSourceException.php | 16 + src/Handlers/AbstractHandler.php | 47 --- src/Handlers/ItemsPerPageHandler.php | 60 --- src/Handlers/LastPageHandler.php | 37 -- src/Handlers/NextPageHandler.php | 63 --- src/Handlers/TotalItemsHandler.php | 40 -- src/Handlers/TotalPagesHandler.php | 35 -- src/LazyJsonPages.php | 52 +++ src/Macro.php | 27 -- src/Outcome.php | 88 ---- src/Paginations/AnyPagination.php | 10 + src/Paginations/Pagination.php | 30 ++ .../LazyJsonPagesServiceProvider.php | 25 -- .../ConfigFactory.php} | 205 ++++------ src/Source.php | 79 ---- src/SourceWrapper.php | 111 ----- src/Sources/AnySource.php | 93 +++++ src/Sources/Source.php | 15 + tests/ConfigTest.php | 175 -------- tests/FixturesAware.php | 41 -- tests/LazyJsonPagesTest.php | 382 ------------------ tests/SourceWrapperTest.php | 97 ----- tests/fixtures/page1.json | 30 -- tests/fixtures/page2.json | 30 -- tests/fixtures/page3.json | 24 -- tests/fixtures/per_page.json | 18 - 45 files changed, 496 insertions(+), 1915 deletions(-) create mode 100644 bootstrap.php create mode 100644 duster.json create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon create mode 100644 pint.json delete mode 100644 src/Concerns/HandlesTotalPages.php delete mode 100644 src/Concerns/RetriesHttpRequests.php create mode 100644 src/Dtos/Config.php delete mode 100644 src/Exceptions/OutOfAttemptsException.php create mode 100644 src/Exceptions/UnsupportedSourceException.php delete mode 100644 src/Handlers/AbstractHandler.php delete mode 100644 src/Handlers/ItemsPerPageHandler.php delete mode 100644 src/Handlers/LastPageHandler.php delete mode 100644 src/Handlers/NextPageHandler.php delete mode 100644 src/Handlers/TotalItemsHandler.php delete mode 100644 src/Handlers/TotalPagesHandler.php create mode 100644 src/LazyJsonPages.php delete mode 100644 src/Macro.php delete mode 100644 src/Outcome.php create mode 100644 src/Paginations/AnyPagination.php create mode 100644 src/Paginations/Pagination.php delete mode 100644 src/Providers/LazyJsonPagesServiceProvider.php rename src/{Config.php => Services/ConfigFactory.php} (55%) delete mode 100644 src/Source.php delete mode 100644 src/SourceWrapper.php create mode 100644 src/Sources/AnySource.php create mode 100644 src/Sources/Source.php delete mode 100644 tests/ConfigTest.php delete mode 100644 tests/FixturesAware.php delete mode 100644 tests/LazyJsonPagesTest.php delete mode 100644 tests/SourceWrapperTest.php delete mode 100644 tests/fixtures/page1.json delete mode 100644 tests/fixtures/page2.json delete mode 100644 tests/fixtures/page3.json delete mode 100644 tests/fixtures/per_page.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0eb215b..af174ad 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ github: cerbero90 +ko_fi: cerbero90 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c613d0..0845105 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,11 +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 + php-version: 8.1 extensions: dom, curl, libxml, mbstring, zip tools: composer:v2 coverage: xdebug @@ -67,17 +58,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 +76,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/ + - 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/.scrutinizer.yml b/.scrutinizer.yml index 5d63b30..359494c 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,4 +1,6 @@ build: + environment: + php: 8.2 nodes: analysis: project_setup: diff --git a/CHANGELOG.md b/CHANGELOG.md index d837b00..234f4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to `lazy-json-pages` will be documented in this file. -Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## 1.0.0 - 2021-08-08 diff --git a/README.md b/README.md index bc0eedd..21a7dbc 100644 --- a/README.md +++ b/README.md @@ -2,45 +2,44 @@ [![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 using asynchronous HTTP requests and generators to load paginated items of JSON APIs into [Laravel lazy collections](https://laravel.com/docs/collections#lazy-collections). +```php +$lazyCollection = LazyCollection::fromJsonPages($source, fn (Config $config) => $config + ->dot('data.results') + ->pages('total_pages') + ->perPage(500, 'page_size') + ->chunk(3) + ->timeout(15) + ->attempts(5) + ->backoff(fn (int $attempt) => $attempt ** 2 * 100)); +``` -Need to load heavy JSON with no pagination? Consider using [Lazy JSON](https://github.com/cerbero90/lazy-json) instead. +Framework-agnostic package to load items from any paginated JSON API into a [Laravel lazy collection](https://laravel.com/docs/collections#lazy-collections) via async HTTP requests. +Need to read large JSON with no pagination in a memory-efficient way? Consider using [๐Ÿผ Lazy JSON](https://github.com/cerbero90/lazy-json) or [๐Ÿงฉ JSON Parser](https://github.com/cerbero90/json-parser) instead. -## Install +## ๐Ÿ“ฆ Install -In a Laravel application, all you need to do is requiring the package: +Via Composer: ``` bash composer require cerbero/lazy-json-pages ``` -Otherwise, you also need to register the lazy collection macro: +## ๐Ÿ”ฎ Usage -``` php -use Cerbero\LazyJsonPages\Macro; -use Illuminate\Support\LazyCollection; - -LazyCollection::macro('fromJsonPages', new Macro()); -``` - -## Usage - -- [Length-aware paginations](#length-aware-paginations) -- [Cursor and next-page paginations](#cursor-and-next-page-paginations) -- [Fine-tuning the pages fetching process](#fine-tuning-the-pages-fetching-process) -- [Handling errors](#handling-errors) +- [๐Ÿ“ Length-aware paginations](#-length-aware-paginations) +- [โ†ช๏ธ Cursor and next-page paginations](#-cursor-and-next-page-paginations) +- [๐Ÿ›  Requests fine-tuning](#-requests-fine-tuning) +- [๐Ÿ’ข Errors handling](#-errors-handling) Loading paginated items of JSON APIs into a lazy collection is possible by calling the collection itself or the included helper: @@ -109,7 +108,7 @@ lazyJsonPages($source, $path, function (Config $config) { The configuration depends on the type of pagination. Various paginations are supported, including length-aware and cursor paginations. -### Length-aware paginations +### ๐Ÿ“ Length-aware paginations The term "length-aware" indicates all paginations that show at least one of the following numbers: - the total number of pages @@ -198,7 +197,7 @@ $config->lastPage('last_page_key')->perPage(500, 'page_size'); ``` -### Cursor and next-page paginations +### โ†ช๏ธ Cursor and next-page paginations Some APIs show only the number or cursor of the next page in all pages. We can tackle this kind of pagination by indicating the JSON key holding the next page: @@ -209,7 +208,7 @@ $config->nextPage('next_page_key'); The JSON key may hold a number, a cursor or a URL, Lazy JSON Pages supports all of them. -### Fine-tuning the pages fetching process +### ๐Ÿ›  Requests fine-tuning Lazy JSON Pages provides a number of settings to adjust the way HTTP requests are sent to fetch pages. For example pages can be requested in chunks, so that only a few streams are kept in memory at once: @@ -275,7 +274,7 @@ $items ``` -### Handling errors +### ๐Ÿ’ข Errors handling As seen above, we can mitigate potentially faulty HTTP requests with backoffs, timeouts and retries. When we reach the maximum number of attempts and a request keeps failing, an `OutOfAttemptsException` is thrown. @@ -296,56 +295,51 @@ try { } ``` - -## 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-pages?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-pages.svg?label=version&style=flat-square -[ico-actions]: https://img.shields.io/github/workflow/status/cerbero90/lazy-json-pages/build?style=flat-square&logo=github +[ico-actions]: https://img.shields.io/github/actions/workflow/status/cerbero90/lazy-json-pages/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-pages.svg?style=flat-square&logo=scrutinizer [ico-code-quality]: https://img.shields.io/scrutinizer/g/cerbero90/lazy-json-pages.svg?style=flat-square&logo=scrutinizer +[ico-phpstan]: https://img.shields.io/badge/level-max-success?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGb0lEQVR42u1Xe1BUZRS/y4Kg8oiR3FCCBUySESZBRCiaBnmEsOzeSzsg+KxYYO9dEEftNRqZjx40FRZkTpqmOz5S2LsXlEZBciatkQnHDGYaGdFy1EpGMHl/p/PdFlt2rk5O+J9n5nA/vtf5ned3lnlISpRhafBlLRLHCtJGVrB/ZBDsaw2lUqzReGAC46DstTYfnSCGUjaaDvgxACo6j3vUenNdImeRXqdnWV5az5rrnzeZznj8J+E5Ftsclhf3s4J4CS/oRx5Bvon8ZU65FGYQxAwcf85a7CeRz+C41THejueydCZ7AAK34nwv3kHP/oUKdOL4K7258fF7Cud427O48RQeGkIGJ77N8fZqlrcfRP4d/x90WQfHXLeBt9dTrSlwl3V65ynWLM1SEA2qbNQckbe4Xmww10Hmy3shid0CMcmlEJtSDsl5VZBdfAgMvI3uuR+moJqN6LaxmpsOBeLCDmTifCB92RcQmbAUJvtqALc5sQr8p86gYBCcFdBq9wOin7NQax6ewlB6rqLZHf23FP10y3lj6uJtEBg2HxiVCtzd3SEwMBCio6Nh9uzZ4O/vLwOZ4OUNM2NyIGPFrvuzBG//lRPs+VQ2k1ki+ePkd84bskz7YFpYgizEz88P8vPzYffu3dDS0gJNTU1QXV0NqampRK1WIwgfiE4qhOyig0rC+pCvK8QUoML7uJVHA5kcQUp3DSpqWjc3d/Dy8oKioiLo6uqCoaEhuHb1KvT09AAhBFpbW4lOpyMyyIBQSCmoUQLQzgniNvz+obB2HS2RwBgE6dOxCyJogmNkP2u1Wrhw4QJ03+iGrR9XEd3CTNBn6eCbo40wPDwMdXV1BF1DVG5qiEtboxSUP6J71+D3NwUAhLOIRQzm7lnnhYUv7QFv/yDZ/Lm5ubK2DVI9iZ8bR8JDtEB57lNzENQN6OjoIGlpabIVZsYaMTO+hrikRRA1JxmSX9hE7/sJtVyF38tKsUCVZxBhz9jI3wGT/QJlADzPAyXrnj0kInzGHQCRMyOg/ed2uHjxIuE4TgYQHq2DLJqumashY+lnsMC4GVC5do6XVuK9l+4SkN8y+GfYeVJn2g++U7QygPT0dBgYGIDvT58mnF5PQcjC83PzSF9fH7S1tZGEhAQZQOT8JaA317oIkM6jS8uVLSDzOQqg23Uh+MlkOf00Gg0cP34c+vv74URzM9n41gby/rvvkc7OThlATU3NCGYJUXt4QaLuTYwBcTSOBmj1RD7D4Tsix4ByOjZRF/zgupDEbgZ3j4ly/qekpND0o5aQ44HS4OAgsVqtI1gTZO01IbG0aP1bknnxCDUvArHi+B0lJSlzglTFYO2udF3Ql9TCrHn5oEIreHp6QlRUFJSUlJCqqipSWVlJ8vLyCGYIFS7HS3zGa87mv4lcjLwLlStlLTKYYUUAlvrlDGcW45wKxXX6aqHZNutM+1oQBHFTewAKkoH4+vqCj48PYAGS5yb5amjNoO+CU2SL53NKpDD0vxHHmOJir7L5xUvZgm0us2R142ScOIyVqYvlpWU4XoHIP8DXL2b+wjdWeXh6U2FjmIIKmbWAYPFRMus62h/geIvjOQYlpuDysQrLL6Ger49HgW8jqvXUhI7UvDb9iaSTDqHtyItiF5Suw5ewF/Nd8VJ6zlhsn06bEhwX4NyfCvuGEeRpTmh4mkG68yDpyuzB9EUcjU5awbAgncPlAeSdAQER0zCndzqVbeXC4qDsMpvGEYBXRnsDx4N3Auf1FCTjTIaVtY/QTmd0I8bBVm1kejEubUfO01vqImn3c49X7qpeqI9inIgtbpxK3YrKfIJCt+OeV2nfUVFR4ca4EkVENyA7gkYcMfB1R5MMmxZ7ez/2KF5SSN1yV+158UPsJT0ZBcI2bRLtIXGoYu5FerOUiJe1OfsL3XEWH43l2KS+iJF9+S4FpcNgsc+j8cT8H4o1bfPg/qkLt50uJ1RzdMsGg0UqwfEN114Pwb1CtWTGg+Y9U5ClK9x7xUWI7BI5VQVp0AVcQ3bZkQhmnEgdHhKyNSZe16crtBIlc7sIb6cRLft2PCgoKGjijBDtjrAQ7a3EdMsxzIRflAFIhPb6mHYmYwX+WBlPQgskhgVryyJCQyNyBLsBQdQ6fgsQhyt6MSOOsWZ7gbH8wETmgRKAijatNL8Ngm0xx4tLcsps0Wzx4al0jXlI40B/A3pa144MDtSgAAAAAElFTkSuQmCC [ico-downloads]: https://img.shields.io/packagist/dt/cerbero/lazy-json-pages.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-pages [link-actions]: https://github.com/cerbero90/lazy-json-pages/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-pages/code-structure [link-code-quality]: https://scrutinizer-ci.com/g/cerbero90/lazy-json-pages [link-downloads]: https://packagist.org/packages/cerbero/lazy-json-pages +[link-phpstan]: https://phpstan.org/ [link-contributors]: ../../contributors diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..c658bd3 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,10 @@ +=6.0" + "php": "^8.1", + "cerbero/lazy-json": "^2.0", + "guzzlehttp/guzzle": "^7.2" }, "require-dev": { - "illuminate/http": ">=6.0", + "illuminate/http": ">=6.20", "mockery/mockery": "^1.3.4", - "phpunit/phpunit": ">=8.0", - "squizlabs/php_codesniffer": "^3.0" + "orchestra/testbench": ">=7.0", + "pestphp/pest": "^2.0", + "phpstan/phpstan": "^1.9", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.0", + "tightenco/duster": "^2.0" }, "autoload": { "psr-4": { "Cerbero\\LazyJsonPages\\": "src" }, "files": [ + "bootstrap.php", "helpers.php" ] }, @@ -45,21 +49,19 @@ } }, "scripts": { - "test": "phpunit", - "check-style": "phpcs --standard=PSR12 src", - "fix-style": "phpcbf --standard=PSR12 src" + "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\\LazyJsonPages\\Providers\\LazyJsonPagesServiceProvider" - ] } }, "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 700f49f..26dd141 100644 --- a/helpers.php +++ b/helpers.php @@ -1,18 +1,18 @@ - - - - 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/HandlesTotalPages.php b/src/Concerns/HandlesTotalPages.php deleted file mode 100644 index 5a8ee82..0000000 --- a/src/Concerns/HandlesTotalPages.php +++ /dev/null @@ -1,109 +0,0 @@ -config->source->request->getUri(); - $firstPageAlreadyFetched = strval($uri) == strval($this->config->source->request->getUri()); - $chunkedPages = $this->chunkPages($pages, $firstPageAlreadyFetched); - $items = $this->fetchItemsAsynchronously($chunkedPages, $uri); - - if ($firstPageAlreadyFetched) { - yield from $this->config->source->json($this->config->path); - } - - yield from $items; - } - - /** - * Retrieve the given pages in chunks - * - * @param int $pages - * @param bool $skipFirstPage - * @return iterable - */ - protected function chunkPages(int $pages, bool $skipFirstPage): iterable - { - $firstPage = $skipFirstPage ? $this->config->firstPage + 1 : $this->config->firstPage; - $lastPage = $this->config->firstPage == 0 ? $pages - 1 : $pages; - - return LazyCollection::range($firstPage, $lastPage)->chunk($this->config->chunk ?: INF); - } - - /** - * Fetch items by performing asynchronous HTTP calls - * - * @param iterable $chunkedPages - * @param Uri $uri - * @return Traversable - */ - protected function fetchItemsAsynchronously(iterable $chunkedPages, Uri $uri): Traversable - { - $client = new Client(['timeout' => $this->config->timeout]); - - foreach ($chunkedPages as $pages) { - $outcome = $this->retry(function (Outcome $outcome) use ($uri, $client, $pages) { - $pages = $outcome->pullFailedPages() ?: $pages; - - return $this->pool($client, $outcome, function () use ($uri, $pages) { - $request = clone $this->config->source->request; - - foreach ($pages as $page) { - yield $page => $request->withUri(Uri::withQueryValue($uri, $this->config->pageName, $page)); - } - }); - }); - - yield from $outcome->pullItems(); - } - } - - /** - * Retrieve the outcome of a pool of asynchronous requests - * - * @param Client $client - * @param Outcome $outcome - * @param callable $getRequests - * @return Outcome - */ - protected function pool(Client $client, Outcome $outcome, callable $getRequests): Outcome - { - $pool = new Pool($client, $getRequests(), [ - 'concurrency' => $this->config->concurrency, - 'fulfilled' => function (ResponseInterface $response, int $page) use ($outcome) { - $outcome->addItemsFromPage($page, $response, $this->config->path); - }, - 'rejected' => function (Throwable $e, int $page) use ($outcome) { - $outcome->addFailedPage($page); - throw $e; - } - ]); - - $pool->promise()->wait(); - - return $outcome; - } -} diff --git a/src/Concerns/RetriesHttpRequests.php b/src/Concerns/RetriesHttpRequests.php deleted file mode 100644 index f722170..0000000 --- a/src/Concerns/RetriesHttpRequests.php +++ /dev/null @@ -1,88 +0,0 @@ -config->attempts; - - do { - $attempt++; - $remainingAttempts--; - - try { - return $callback($outcome); - } catch (Throwable $e) { - if ($remainingAttempts > 0) { - $this->backoff($attempt); - } else { - throw new OutOfAttemptsException($e, $outcome); - } - } - } while ($remainingAttempts > 0); - } - - /** - * Execute the backoff strategy - * - * @param int $attempt - * @return void - */ - protected function backoff(int $attempt): void - { - $backoff = $this->config->backoff ?: function (int $attempt) { - return ($attempt - 1) ** 2 * 1000; - }; - - usleep($backoff($attempt) * 1000); - } - - /** - * Retry to yield the result of HTTP requests - * - * @param callable $callback - * @return mixed - */ - protected function retryYielding(callable $callback) - { - $attempt = 0; - $outcome = new Outcome(); - $remainingAttempts = $this->config->attempts; - - do { - $failed = false; - $attempt++; - $remainingAttempts--; - - try { - yield from $callback($outcome); - } catch (Throwable $e) { - $failed = true; - - if ($remainingAttempts > 0) { - $this->backoff($attempt); - } else { - throw new OutOfAttemptsException($e, $outcome); - } - } - } while ($failed && $remainingAttempts > 0); - } -} diff --git a/src/Dtos/Config.php b/src/Dtos/Config.php new file mode 100644 index 0000000..761a4ff --- /dev/null +++ b/src/Dtos/Config.php @@ -0,0 +1,27 @@ +getMessage()); + } } diff --git a/src/Exceptions/OutOfAttemptsException.php b/src/Exceptions/OutOfAttemptsException.php deleted file mode 100644 index e178cbc..0000000 --- a/src/Exceptions/OutOfAttemptsException.php +++ /dev/null @@ -1,51 +0,0 @@ -message = $original->getMessage(); - $this->original = $original; - $this->failedPages = $outcome->pullFailedPages(); - $this->items = new LazyCollection(function () use ($outcome) { - yield from $outcome->pullItems(); - }); - } -} diff --git a/src/Exceptions/UnsupportedSourceException.php b/src/Exceptions/UnsupportedSourceException.php new file mode 100644 index 0000000..a9614f6 --- /dev/null +++ b/src/Exceptions/UnsupportedSourceException.php @@ -0,0 +1,16 @@ +config = $config; - } - - /** - * Determine whether the handler can handle the APIs configuration - * - * @return bool - */ - abstract public function matches(): bool; - - /** - * Handle the APIs configuration - * - * @return Traversable - */ - abstract public function handle(): Traversable; -} diff --git a/src/Handlers/ItemsPerPageHandler.php b/src/Handlers/ItemsPerPageHandler.php deleted file mode 100644 index 77df0b2..0000000 --- a/src/Handlers/ItemsPerPageHandler.php +++ /dev/null @@ -1,60 +0,0 @@ -config->perPageQuery - && $this->config->nextPageKey === null - && ($this->config->items || $this->config->pages || $this->config->lastPage); - } - - /** - * Handle the APIs configuration - * - * @return Traversable - */ - public function handle(): Traversable - { - $originalUri = $this->config->source->request->getUri(); - $pages = (int) ceil($this->countItems() / $this->config->perPageOverride); - $uri = Uri::withQueryValue($originalUri, $this->config->perPageQuery, $this->config->perPageOverride); - - yield from $this->handleByTotalPages($pages, $uri); - } - - /** - * Retrieve the total number of items - * - * @return int - */ - protected function countItems(): int - { - if ($this->config->items) { - return $this->config->items; - } elseif ($this->config->pages) { - return $this->config->pages * $this->config->perPage; - } - - $pages = $this->config->firstPage == 0 ? $this->config->lastPage + 1 : $this->config->lastPage; - - return $pages * $this->config->perPage; - } -} diff --git a/src/Handlers/LastPageHandler.php b/src/Handlers/LastPageHandler.php deleted file mode 100644 index 42a62ef..0000000 --- a/src/Handlers/LastPageHandler.php +++ /dev/null @@ -1,37 +0,0 @@ -config->lastPage > 0 && $this->config->perPageQuery === null; - } - - /** - * Handle the APIs configuration - * - * @return Traversable - */ - public function handle(): Traversable - { - $pages = $this->config->firstPage == 0 ? $this->config->lastPage + 1 : $this->config->lastPage; - - yield from $this->handleByTotalPages($pages); - } -} diff --git a/src/Handlers/NextPageHandler.php b/src/Handlers/NextPageHandler.php deleted file mode 100644 index b83d498..0000000 --- a/src/Handlers/NextPageHandler.php +++ /dev/null @@ -1,63 +0,0 @@ -config->nextPageKey; - } - - /** - * Handle the APIs configuration - * - * @return Traversable - */ - public function handle(): Traversable - { - yield from $this->retryYielding(function (Outcome $outcome) { - try { - yield from $this->handleByNextPage(); - } catch (Throwable $e) { - $outcome->pullFailedPages(); - $outcome->addFailedPage($this->config->nextPage); - throw $e; - } - }); - } - - /** - * Handle APIs with next page - * - * @return Traversable - */ - protected function handleByNextPage(): Traversable - { - $request = clone $this->config->source->request; - - yield from $this->config->source->json($this->config->path); - - while ($this->config->nextPage) { - $uri = Uri::withQueryValue($request->getUri(), $this->config->pageName, $this->config->nextPage); - $this->config->source = new SourceWrapper($request->withUri($uri)); - $this->config->nextPage($this->config->nextPageKey); - yield from $this->handleByNextPage(); - } - } -} diff --git a/src/Handlers/TotalItemsHandler.php b/src/Handlers/TotalItemsHandler.php deleted file mode 100644 index c59dbb7..0000000 --- a/src/Handlers/TotalItemsHandler.php +++ /dev/null @@ -1,40 +0,0 @@ -config->items > 0 - && $this->config->pages === null - && $this->config->perPageQuery === null; - } - - /** - * Handle the APIs configuration - * - * @return Traversable - */ - public function handle(): Traversable - { - $perPage = $this->config->perPage ?? count($this->config->source->json($this->config->path)); - $pages = $perPage > 0 ? (int) ceil($this->config->items / $perPage) : 0; - - yield from $this->handleByTotalPages($pages); - } -} diff --git a/src/Handlers/TotalPagesHandler.php b/src/Handlers/TotalPagesHandler.php deleted file mode 100644 index 20968e3..0000000 --- a/src/Handlers/TotalPagesHandler.php +++ /dev/null @@ -1,35 +0,0 @@ -config->pages > 0 && $this->config->perPageQuery === null; - } - - /** - * Handle the APIs configuration - * - * @return Traversable - */ - public function handle(): Traversable - { - yield from $this->handleByTotalPages($this->config->pages); - } -} diff --git a/src/LazyJsonPages.php b/src/LazyJsonPages.php new file mode 100644 index 0000000..c14e9dc --- /dev/null +++ b/src/LazyJsonPages.php @@ -0,0 +1,52 @@ + + */ +final class LazyJsonPages implements IteratorAggregate +{ + /** + * @param Closure(ConfigFactory): void $configure + * @return LazyCollection + */ + public static function from(mixed $source, Closure $configure): LazyCollection + { + $source = new AnySource($source); + $configure($config = new ConfigFactory($source)); + + return new LazyCollection(fn() => yield from new self($source, $config->make())); + } + + private function __construct( + private readonly AnySource $source, + private readonly Config $config, + ) { + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + try { + yield from new AnyPagination($this->source, $this->config); + } catch (Throwable $e) { + throw LazyJsonPagesException::from($e); + } + } +} diff --git a/src/Macro.php b/src/Macro.php deleted file mode 100644 index 5b9ef3c..0000000 --- a/src/Macro.php +++ /dev/null @@ -1,27 +0,0 @@ -items[$page] = (function () use ($response, $path) { - yield from new Source($response, $path); - })(); - - return $this; - } - - /** - * Traverse and unset the items - * - * @return Traversable - */ - public function pullItems(): Traversable - { - ksort($this->items); - - foreach ($this->items as $generator) { - yield from $generator; - } - - $this->items = []; - } - - /** - * Add the given page to the failed pages - * - * @param string|int $page - * @return self - */ - public function addFailedPage($page): self - { - $this->failedPages[] = $page; - - return $this; - } - - /** - * Retrieve and unset the failed pages - * - * @return array - */ - public function pullFailedPages(): array - { - $failedPages = $this->failedPages; - - $this->failedPages = []; - - return $failedPages; - } -} diff --git a/src/Paginations/AnyPagination.php b/src/Paginations/AnyPagination.php new file mode 100644 index 0000000..fd117ff --- /dev/null +++ b/src/Paginations/AnyPagination.php @@ -0,0 +1,10 @@ + + */ +class pagination implements IteratorAggregate +{ + public final function __construct( + protected readonly AnySource $source, + protected readonly Config $config, + ) { + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + yield 1; + } +} diff --git a/src/Providers/LazyJsonPagesServiceProvider.php b/src/Providers/LazyJsonPagesServiceProvider.php deleted file mode 100644 index f5953ad..0000000 --- a/src/Providers/LazyJsonPagesServiceProvider.php +++ /dev/null @@ -1,25 +0,0 @@ -source = new SourceWrapper($source); - $this->path = $path; - $this->hydrateConfig($config); } /** - * Hydrate the configuration - * - * @param callable|array|string|int $config - * @return void - * - * @throws LazyJsonPagesException + * Set the dot-notation path to extract items from */ - protected function hydrateConfig($config): void + public function dot(string $dot): self { - if (is_callable($config)) { - $config($this); - } elseif (is_array($config)) { - $this->resolveConfig($config); - } elseif (is_string($config) || is_numeric($config)) { - $this->pages($config); - } else { - throw new LazyJsonPagesException('The provided configuration is not valid.'); - } - } + $this->dot = $dot; - /** - * Resolve the given configuration - * - * @param array $config - * @return void - * - * @throws LazyJsonPagesException - */ - protected function resolveConfig(array $config): void - { - foreach ($config as $key => $value) { - if (method_exists($this, $method = Str::camel($key))) { - $values = is_array($value) ? $value : [$value]; - call_user_func_array([$this, $method], $values); - } else { - throw new LazyJsonPagesException("The key [{$key}] is not valid."); - } - } + return $this; } /** - * Set the page name - * - * @param string $name - * @return self + * Set the name of the page */ public function pageName(string $name): self { @@ -216,17 +145,24 @@ public function firstPage(int $page): self /** * Set the total number of pages - * - * @param string|int $pages - * @return self */ - public function pages($pages): self + public function totalPages(Closure|string $totalPages): self { - $this->pages = $this->resolveInt($pages, 1); + $this->totalPages = $this->integerFromResponse($totalPages, minimum: 1); + + // $this->totalPages = $this->extractor->integerFromResponse($totalPages); return $this; } + private function integerFromResponse(Closure|string $key, int $minimum = 0): int + { + return (int) max($minimum, match (true) { + $key instanceof Closure => $key($this->source->response()), + default => $this->source->response()->json($key) ?? $this->source->response()->header($key), + }); + } + /** * Retrieve an integer from the given value * @@ -236,7 +172,7 @@ public function pages($pages): self */ protected function resolveInt($value, int $minimum): int { - return (int) max($minimum, $this->resolvePage($value)); + return max($minimum, (int) $this->resolvePage($value)); } /** @@ -259,39 +195,28 @@ protected function resolvePage($value) /** * Set the total number of items - * - * @param string|int $items - * @return self */ - public function items($items): self + public function totalItems(Closure|string $totalItems): self { - $this->items = $this->resolveInt($items, 0); + $this->totalItems = $this->integerFromResponse($totalItems); return $this; } /** * Set the number of items per page and optionally override it - * - * @param int $perPage - * @param string|null $query - * @param int $firstPageItems - * @return self */ - public function perPage(int $perPage, string $query = null, int $firstPageItems = 1): self + public function perPage(int $perPage, ?string $key = null, int $firstPageItems = 1): self { - $this->perPage = max(1, $query ? $firstPageItems : $perPage); - $this->perPageQuery = $query; - $this->perPageOverride = $query ? max(1, $perPage) : null; + $this->perPage = max(1, $key ? $firstPageItems : $perPage); + $this->perPageKey = $key; + $this->perPageOverride = $key ? max(1, $perPage) : null; return $this; } /** * Set the next page - * - * @param string $key - * @return self */ public function nextPage(string $key): self { @@ -301,6 +226,14 @@ public function nextPage(string $key): self return $this; } + private function pageFromResponse(Closure|string $key, int $minimum = 0): string|int + { + return (int) max($minimum, match (true) { + $key instanceof Closure => $key($this->source->response()), + default => $this->source->response()->json($key) ?? $this->source->response()->header($key), + }); + } + /** * Set the number of the last page * @@ -388,4 +321,26 @@ public function backoff(callable $callback): self return $this; } + + public function make(): Config + { + return new Config( + $this->dot, + $this->pageName, + $this->firstPage, + $this->totalPages, + $this->totalItems, + $this->perPage, + $this->perPageKey, + $this->perPageOverride, + $this->nextPage, + $this->nextPageKey, + $this->lastPage, + $this->chunk, + $this->concurrency, + $this->timeout, + $this->attempts, + $this->backoff, + ); + } } diff --git a/src/Source.php b/src/Source.php deleted file mode 100644 index cb61185..0000000 --- a/src/Source.php +++ /dev/null @@ -1,79 +0,0 @@ -traversable = $this->toTraversable(new Config($source, $path, $config)); - } - - /** - * Retrieve the traversable items depending on the given configuration - * - * @param Config $config - * @return Traversable - * - * @throws LazyJsonPagesException - */ - protected function toTraversable(Config $config): Traversable - { - foreach ($this->handlers as $class) { - /** @var Handlers\AbstractHandler $handler */ - $handler = new $class($config); - - if ($handler->matches()) { - return $handler->handle(); - } - } - - throw new LazyJsonPagesException('Unable to load paginated items from the provided source.'); - } - - /** - * Retrieve the traversable items - * - * @return Traversable - */ - public function getIterator(): Traversable - { - return $this->traversable; - } -} diff --git a/src/SourceWrapper.php b/src/SourceWrapper.php deleted file mode 100644 index eee7190..0000000 --- a/src/SourceWrapper.php +++ /dev/null @@ -1,111 +0,0 @@ -original = $source; - $this->request = $this->getSourceRequest(); - $this->response = $this->getSourceResponse(); - } - - /** - * Retrieve the HTTP request of the source - * - * @return RequestInterface - * - * @throws LazyJsonPagesException - */ - protected function getSourceRequest(): RequestInterface - { - if ($this->original instanceof RequestInterface) { - return $this->original; - } elseif (isset($this->original->transferStats)) { - return $this->original->transferStats->getRequest(); - } - - throw new LazyJsonPagesException('The HTTP client response is not aware of the original request.'); - } - - /** - * Retrieve the HTTP response of the source - * - * @return ResponseInterface - */ - protected function getSourceResponse(): ResponseInterface - { - if ($this->original instanceof RequestInterface) { - return (new Client())->send($this->original); - } - - return $this->original->toPsrResponse(); - } - - /** - * Retrieve a fragment of the decoded JSON - * - * @param string $path - * @param mixed $default - * @return mixed - */ - public function json(string $path, $default = null) - { - if (!isset($this->decodedJson)) { - $this->decodedJson = json_decode((string) $this->response->getBody(), true); - } - - return Arr::get($this->decodedJson, $path, $default); - } -} diff --git a/src/Sources/AnySource.php b/src/Sources/AnySource.php new file mode 100644 index 0000000..d341b21 --- /dev/null +++ b/src/Sources/AnySource.php @@ -0,0 +1,93 @@ +[] + */ + protected array $supportedSources = [ + CustomSource::class, + Endpoint::class, + LaravelClientResponse::class, + Psr7Request::class, + ]; + + /** + * The matching source. + * + * @var Source|null + */ + protected ?Source $matchingSource; + + /** + * Retrieve the JSON fragments + * + * @return Traversable + * @throws UnsupportedSourceException + */ + public function getIterator(): Traversable + { + return $this->matchingSource(); + } + + /** + * Retrieve the matching source + * + * @return Source + * @throws UnsupportedSourceException + */ + protected function matchingSource(): Source + { + if (isset($this->matchingSource)) { + return $this->matchingSource; + } + + foreach ($this->sources() as $source) { + if ($source->matches()) { + return $this->matchingSource = $source; + } + } + + throw new UnsupportedSourceException($this->source); + } + + /** + * Retrieve all available sources + * + * @return Generator + */ + protected function sources(): Generator + { + foreach ($this->supportedSources as $source) { + yield new $source($this->source, $this->config); + } + } + + /** + * Determine whether the JSON source can be handled + * + * @return bool + */ + public function matches(): bool + { + return true; + } + + /** + * Retrieve the calculated size of the JSON source + * + * @return int|null + */ + protected function calculateSize(): ?int + { + return $this->matchingSource()->size(); + } +} diff --git a/src/Sources/Source.php b/src/Sources/Source.php new file mode 100644 index 0000000..e3b0251 --- /dev/null +++ b/src/Sources/Source.php @@ -0,0 +1,15 @@ +shouldReceive('json')->with('next_page')->andReturn(2); - $wrapper->shouldReceive('json')->with('last_page')->andReturn('https://paginated-json-api.test?page_name=3'); - - $source = new Request('GET', 'https://paginated-json-api.test'); - - $config = new Config($source, 'path', function (Config $config) { - $config - ->pageName('page_name') - ->firstPage(0) - ->pages(123) - ->items(321) - ->perPage(200, 'per_page', 2) - ->nextPage('next_page') - ->lastPage('last_page') - ->chunk(3) - ->concurrency(4) - ->timeout(-1) - ->attempts(6) - ->backoff(function (int $attempt) { - return $attempt; - }); - }); - - $this->assertSame('path', $config->path); - $this->assertSame('page_name', $config->pageName); - $this->assertSame(0, $config->firstPage); - $this->assertSame(123, $config->pages); - $this->assertSame(321, $config->items); - $this->assertSame(2, $config->perPage); - $this->assertSame('per_page', $config->perPageQuery); - $this->assertSame(200, $config->perPageOverride); - $this->assertSame(2, $config->nextPage); - $this->assertSame('next_page', $config->nextPageKey); - $this->assertSame(3, $config->lastPage); - $this->assertSame(3, $config->chunk); - $this->assertSame(4, $config->concurrency); - $this->assertSame(0, $config->timeout); - $this->assertSame(6, $config->attempts); - $this->assertSame(7, ($config->backoff)(7)); - } - - /** - * @test - */ - public function sets_options_through_associative_array() - { - $wrapper = Mockery::mock('overload:' . SourceWrapper::class); - $wrapper->shouldReceive('json')->with('next_page')->andReturn(2); - $wrapper->shouldReceive('json')->with('last_page')->andReturn('https://paginated-json-api.test?page_name=3'); - - $source = new Request('GET', 'https://paginated-json-api.test'); - - $config = new Config($source, 'path', [ - 'pageName' => 'page_name', - 'first_page' => 0, - 'pages' => 123, - 'items' => 321, - 'perPage' => [200, 'per_page', 2], - 'nextPage' => 'next_page', - 'lastPage' => 'last_page', - 'sync' => true, - 'concurrency' => 4, - 'timeout' => -1, - 'attempts' => 6, - 'backoff' => function (int $attempt) { - return $attempt; - } - ]); - - $this->assertSame('path', $config->path); - $this->assertSame('page_name', $config->pageName); - $this->assertSame(0, $config->firstPage); - $this->assertSame(123, $config->pages); - $this->assertSame(321, $config->items); - $this->assertSame(2, $config->perPage); - $this->assertSame('per_page', $config->perPageQuery); - $this->assertSame(200, $config->perPageOverride); - $this->assertSame(2, $config->nextPage); - $this->assertSame('next_page', $config->nextPageKey); - $this->assertSame(3, $config->lastPage); - $this->assertSame(1, $config->chunk); - $this->assertSame(4, $config->concurrency); - $this->assertSame(0, $config->timeout); - $this->assertSame(6, $config->attempts); - $this->assertSame(7, ($config->backoff)(7)); - } - - /** - * @test - */ - public function sets_the_total_pages_with_an_integer() - { - Mockery::mock('overload:' . SourceWrapper::class); - - $source = new Request('GET', 'https://paginated-json-api.test'); - - $config = new Config($source, 'path', 123); - - $this->assertSame(123, $config->pages); - } - - /** - * @test - */ - public function sets_the_total_pages_with_a_json_key() - { - $wrapper = Mockery::mock('overload:' . SourceWrapper::class); - $wrapper->shouldReceive('json')->with('total_pages')->andReturn(3); - - $source = new Request('GET', 'https://paginated-json-api.test'); - - $config = new Config($source, 'path', 'total_pages'); - - $this->assertSame(3, $config->pages); - } - - /** - * @test - */ - public function fails_when_bad_configurations_are_provided() - { - Mockery::mock('overload:' . SourceWrapper::class); - - $this->expectExceptionObject(new LazyJsonPagesException('The provided configuration is not valid.')); - - $source = new Request('GET', 'https://paginated-json-api.test'); - - new Config($source, 'path', new stdClass()); - } - - /** - * @test - */ - public function fails_when_a_bad_option_is_provided() - { - Mockery::mock('overload:' . SourceWrapper::class); - - $this->expectExceptionObject(new LazyJsonPagesException('The key [bad] is not valid.')); - - $source = new Request('GET', 'https://paginated-json-api.test'); - - new Config($source, 'path', ['bad' => true]); - } -} diff --git a/tests/FixturesAware.php b/tests/FixturesAware.php deleted file mode 100644 index c2e107d..0000000 --- a/tests/FixturesAware.php +++ /dev/null @@ -1,41 +0,0 @@ -resolve($this->fixture($fixture)); - }); - } -} diff --git a/tests/LazyJsonPagesTest.php b/tests/LazyJsonPagesTest.php deleted file mode 100644 index 061dfbe..0000000 --- a/tests/LazyJsonPagesTest.php +++ /dev/null @@ -1,382 +0,0 @@ - $this->fixture('page1')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page=3' => $this->promiseFixture('page3'), - ]; - $config = 'meta.pagination.total_pages'; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * Assert that all items are correctly lazy-loaded via the given configuration - * - * @param array $initialRequest - * @param array $asyncRequests - * @param mixed $config - * @param array|null $expectedIds - * @return void - */ - protected function assertAllItemsAreLazyLoaded( - array $initialRequest, - array $asyncRequests, - $config, - array $expectedIds = null - ): void { - $source = new Request('GET', key($initialRequest)); - $client = Mockery::mock('overload:' . Client::class, ClientInterface::class); - - $client->shouldReceive('send')->with($source)->andReturn(reset($initialRequest)); - - foreach ($asyncRequests as $url => $promise) { - $client->shouldReceive('sendAsync') - ->withArgs(function (Request $request) use ($url) { - return $request->getUri() == $url; - }) - ->andReturn($promise); - } - - $index = 0; - $expectedIds = $expectedIds ?: range(1, 13); - - lazyJsonPages($source, 'data.results', $config)->each(function ($item) use (&$index, $expectedIds) { - $this->assertSame($expectedIds[$index], $item['id']); - $index++; - }); - } - - /** - * @test - */ - public function handles_total_items() - { - $initialRequest = ['https://paginated-json-api.test' => $this->fixture('page1')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page=3' => $this->promiseFixture('page3'), - ]; - $config = ['items' => 'meta.pagination.total_items']; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_total_items_with_per_page() - { - $initialRequest = ['https://paginated-json-api.test' => $this->fixture('page1')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page=3' => $this->promiseFixture('page3'), - ]; - $config = [ - 'items' => 'meta.pagination.total_items', - 'per_page' => 5, - ]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_items_per_page_with_total_pages() - { - $initialRequest = ['https://paginated-json-api.test?page_size=1' => $this->fixture('per_page')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page_size=5&page=1' => $this->promiseFixture('page1'), - 'https://paginated-json-api.test?page_size=5&page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page_size=5&page=3' => $this->promiseFixture('page3'), - ]; - $config = [ - 'pages' => 'meta.pagination.total_pages', - 'per_page' => [5, 'page_size'], - ]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_items_per_page_with_total_items() - { - $initialRequest = ['https://paginated-json-api.test?page_size=1' => $this->fixture('per_page')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page_size=5&page=1' => $this->promiseFixture('page1'), - 'https://paginated-json-api.test?page_size=5&page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page_size=5&page=3' => $this->promiseFixture('page3'), - ]; - $config = [ - 'items' => 'meta.pagination.total_items', - 'per_page' => [5, 'page_size'], - ]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_items_per_page_with_last_page() - { - $initialRequest = ['https://paginated-json-api.test?page_size=1' => $this->fixture('per_page')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page_size=5&page=1' => $this->promiseFixture('page1'), - 'https://paginated-json-api.test?page_size=5&page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page_size=5&page=3' => $this->promiseFixture('page3'), - ]; - $config = [ - 'last_page' => 'meta.pagination.last_page', - 'per_page' => [5, 'page_size'], - ]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_items_per_page_with_last_page_and_first_page_equal_to_0() - { - $initialRequest = ['https://paginated-json-api.test?page_size=1' => $this->fixture('per_page')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page_size=5&page=0' => $this->promiseFixture('page1'), - 'https://paginated-json-api.test?page_size=5&page=1' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page_size=5&page=2' => $this->promiseFixture('page3'), - ]; - $config = [ - 'first_page' => 0, - 'last_page' => 'meta.pagination.last_page', - 'per_page' => [5, 'page_size'], - ]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_last_page() - { - $initialRequest = ['https://paginated-json-api.test' => $this->fixture('page1')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page=3' => $this->promiseFixture('page3'), - ]; - $config = [ - 'last_page' => 'meta.pagination.last_page', - ]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function handles_last_page_and_first_page_equal_to_0() - { - $initialRequest = ['https://paginated-json-api.test' => $this->fixture('page1')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page=1' => $this->promiseFixture('page1'), - 'https://paginated-json-api.test?page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page=3' => $this->promiseFixture('page3'), - ]; - $config = [ - 'first_page' => 0, - 'last_page' => 'meta.pagination.last_page', - ]; - $expectedIds = array_merge(range(1, 5), range(1, 13)); - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config, $expectedIds); - } - - /** - * @test - */ - public function handles_next_page() - { - $config = ['next_page' => 'meta.pagination.next_page']; - $source = new Request('GET', 'https://paginated-json-api.test'); - $client = Mockery::mock('overload:' . Client::class, ClientInterface::class); - - $client->shouldReceive('send') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test'; - }) - ->andReturn($this->fixture('page1')); - - $client->shouldReceive('send') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test?page=2'; - }) - ->andReturn($this->fixture('page2')); - - $client->shouldReceive('send') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test?page=3'; - }) - ->andReturn($this->fixture('page3')); - - $index = 0; - $expectedIds = range(1, 13); - - lazyJsonPages($source, 'data.results', $config)->each(function ($item) use (&$index, $expectedIds) { - $this->assertSame($expectedIds[$index], $item['id']); - $index++; - }); - } - - /** - * @test - */ - public function chunks_pages() - { - $initialRequest = ['https://paginated-json-api.test' => $this->fixture('page1')]; - $asyncRequests = [ - 'https://paginated-json-api.test?page=2' => $this->promiseFixture('page2'), - 'https://paginated-json-api.test?page=3' => $this->promiseFixture('page3'), - ]; - $config = ['items' => 'meta.pagination.total_items', 'chunk' => 1]; - - $this->assertAllItemsAreLazyLoaded($initialRequest, $asyncRequests, $config); - } - - /** - * @test - */ - public function fails_if_configuration_does_not_match_with_any_handler() - { - $this->expectException(LazyJsonPagesException::class); - $this->expectExceptionMessage('Unable to load paginated items from the provided source.'); - - $initialRequest = ['https://paginated-json-api.test' => $this->fixture('page1')]; - $config = []; - - $this->assertAllItemsAreLazyLoaded($initialRequest, [], $config); - } - - /** - * @test - */ - public function handles_failures() - { - $source = new Request('GET', 'https://paginated-json-api.test'); - $client = Mockery::mock('overload:' . Client::class, ClientInterface::class); - - $client->shouldReceive('send')->with($source)->andReturn($this->fixture('page1')); - - $client->shouldReceive('sendAsync') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test?page=2'; - }) - ->andReturn($this->promiseFixture('page2')); - - $client->shouldReceive('sendAsync') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test?page=3'; - }) - ->andReturn(new Promise(function () { - throw new Exception('foo'); - })); - - try { - lazyJsonPages($source, 'data.results', 'meta.pagination.total_pages')->each(function () { - // - }); - } catch (Throwable $e) { - $this->assertInstanceOf(OutOfAttemptsException::class, $e); - $this->assertSame('foo', $e->getMessage()); - $this->assertSame([3], $e->failedPages); - $this->assertSame(5, $e->items->count()); - } - } - - /** - * @test - */ - public function handles_next_page_failures() - { - $config = ['next_page' => 'meta.pagination.next_page']; - $source = new Request('GET', 'https://paginated-json-api.test'); - $client = Mockery::mock('overload:' . Client::class, ClientInterface::class); - - $client->shouldReceive('send') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test'; - }) - ->andReturn($this->fixture('page1')); - - $client->shouldReceive('send') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test?page=2'; - }) - ->andReturn($this->fixture('page2')); - - $client->shouldReceive('send') - ->withArgs(function (Request $request) { - return $request->getUri() == 'https://paginated-json-api.test?page=3'; - }) - ->andThrow(new Exception('foo')); - - try { - lazyJsonPages($source, 'data.results', $config)->each(function () { - // - }); - } catch (Throwable $e) { - $this->assertInstanceOf(OutOfAttemptsException::class, $e); - $this->assertSame('foo', $e->getMessage()); - $this->assertSame([3], $e->failedPages); - $this->assertSame(0, $e->items->count()); - } - } -} diff --git a/tests/SourceWrapperTest.php b/tests/SourceWrapperTest.php deleted file mode 100644 index 1d26945..0000000 --- a/tests/SourceWrapperTest.php +++ /dev/null @@ -1,97 +0,0 @@ -expectExceptionObject(new LazyJsonPagesException('The provided JSON source is not valid.')); - - new SourceWrapper(123); - } - - /** - * @test - * @runInSeparateProcess - */ - public function sets_response_and_request_from_a_psr7_request() - { - $source = new Request('GET', 'https://paginated-json-api.test'); - $client = Mockery::mock('overload:' . Client::class, ClientInterface::class); - $response = $this->fixture('page1'); - - $client->shouldReceive('send')->with($source)->andReturn($response); - - $wrapper = new SourceWrapper($source); - - $this->assertSame($source, $wrapper->original); - $this->assertSame($source, $wrapper->request); - $this->assertSame($response, $wrapper->response); - } - - /** - * @test - */ - public function sets_response_and_request_from_a_laravel_http_client_response() - { - if (!class_exists(Response::class)) { - $this->markTestSkipped('The Laravel HTTP client response class is required for this test.'); - } - - $response = $this->fixture('page1'); - $source = new Response($response); - $request = new Request('GET', 'https://paginated-json-api.test'); - $source->transferStats = new TransferStats($request); - - $wrapper = new SourceWrapper($source); - - $this->assertSame($source, $wrapper->original); - $this->assertSame($request, $wrapper->request); - $this->assertSame($response, $wrapper->response); - } - - /** - * @test - */ - public function fails_if_laravel_response_does_not_have_transfer_stats() - { - if (!class_exists(Response::class)) { - $this->markTestSkipped('The Laravel HTTP client response class is required for this test.'); - } - - $this->expectException(LazyJsonPagesException::class); - $this->expectExceptionMessage('The HTTP client response is not aware of the original request.'); - - $source = new Response($this->fixture('page1')); - - new SourceWrapper($source); - } -} diff --git a/tests/fixtures/page1.json b/tests/fixtures/page1.json deleted file mode 100644 index 98dfae0..0000000 --- a/tests/fixtures/page1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "data": { - "results": [ - { - "id": 1 - }, - { - "id": 2 - }, - { - "id": 3 - }, - { - "id": 4 - }, - { - "id": 5 - } - ] - }, - "meta": { - "pagination": { - "total_pages": 3, - "total_items": 13, - "last_page": 3, - "next_page": 2, - "next_page_url": "https://paginated-json-api.test?page=2" - } - } -} diff --git a/tests/fixtures/page2.json b/tests/fixtures/page2.json deleted file mode 100644 index 5db4935..0000000 --- a/tests/fixtures/page2.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "data": { - "results": [ - { - "id": 6 - }, - { - "id": 7 - }, - { - "id": 8 - }, - { - "id": 9 - }, - { - "id": 10 - } - ] - }, - "meta": { - "pagination": { - "total_pages": 3, - "total_items": 13, - "last_page": 3, - "next_page": 3, - "next_page_url": "https://paginated-json-api.test?page=3" - } - } -} diff --git a/tests/fixtures/page3.json b/tests/fixtures/page3.json deleted file mode 100644 index c291478..0000000 --- a/tests/fixtures/page3.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "data": { - "results": [ - { - "id": 11 - }, - { - "id": 12 - }, - { - "id": 13 - } - ] - }, - "meta": { - "pagination": { - "total_pages": 3, - "total_items": 13, - "last_page": 3, - "next_page": null, - "next_page_url": null - } - } -} diff --git a/tests/fixtures/per_page.json b/tests/fixtures/per_page.json deleted file mode 100644 index 6e8ad3a..0000000 --- a/tests/fixtures/per_page.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "data": { - "results": [ - { - "id": 1 - } - ] - }, - "meta": { - "pagination": { - "total_pages": 13, - "total_items": 13, - "last_page": 13, - "next_page": 2, - "next_page_url": "https://paginated-json-api.test?page=2" - } - } -}